diff --git a/docker/nginx-https.conf b/docker/nginx-https.conf index 3318d70cd..234454693 100644 --- a/docker/nginx-https.conf +++ b/docker/nginx-https.conf @@ -161,7 +161,7 @@ http { root /app/html; index index.html index.htm; expires off; - add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'wasm-unsafe-eval' 'sha256-zKdkhhPi/Ts2hmKgRCOB9Lfl+DkWJeRhR+a0sSotsBc='; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: http: https:; font-src 'self' data:; connect-src 'self' http: https: ws: wss:; media-src 'self' data: blob: http: https:; worker-src 'self' blob:; frame-src http: https:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'wasm-unsafe-eval' 'sha256-OJgSdz/tftu3bBUxP1DqbBkfBm/fs2VjcBuZm3txHlk='; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: http: https:; font-src 'self' data:; connect-src 'self' http: https: ws: wss:; media-src 'self' data: blob: http: https:; worker-src 'self' blob:; frame-src http: https:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always; add_header X-Frame-Options "DENY" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always; @@ -303,15 +303,6 @@ http { proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; } - location ~ ^/terminal(/.*)?$ { - proxy_pass http://127.0.0.1:30001; - proxy_http_version 1.1; - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; - } - location ~ ^/plugins(/.*)?$ { proxy_pass http://127.0.0.1:30001; proxy_http_version 1.1; diff --git a/docker/nginx.conf b/docker/nginx.conf index 5f205e42c..8303ef9b1 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -142,7 +142,7 @@ http { root /app/html; index index.html index.htm; expires off; - add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'wasm-unsafe-eval' 'sha256-zKdkhhPi/Ts2hmKgRCOB9Lfl+DkWJeRhR+a0sSotsBc='; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: http: https:; font-src 'self' data:; connect-src 'self' http: https: ws: wss:; media-src 'self' data: blob: http: https:; worker-src 'self' blob:; frame-src http: https:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'wasm-unsafe-eval' 'sha256-OJgSdz/tftu3bBUxP1DqbBkfBm/fs2VjcBuZm3txHlk='; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: http: https:; font-src 'self' data:; connect-src 'self' http: https: ws: wss:; media-src 'self' data: blob: http: https:; worker-src 'self' blob:; frame-src http: https:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always; add_header X-Frame-Options "DENY" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always; @@ -283,15 +283,6 @@ http { proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; } - location ~ ^/terminal(/.*)?$ { - proxy_pass http://127.0.0.1:30001; - proxy_http_version 1.1; - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; - } - location ~ ^/plugins(/.*)?$ { proxy_pass http://127.0.0.1:30001; proxy_http_version 1.1; diff --git a/drizzle/mysql/0038_quiet_karnak.sql b/drizzle/mysql/0038_quiet_karnak.sql new file mode 100644 index 000000000..c6b92fab6 --- /dev/null +++ b/drizzle/mysql/0038_quiet_karnak.sql @@ -0,0 +1,5 @@ +-- command_history moved to the ssh-terminal plugin, whose first migration +-- renames it to p_ssh_terminal_command_history. Dropping it here would lose +-- every saved command, so this only stops drizzle tracking it. SELECT 1 +-- because MySQL rejects an empty query. +SELECT 1; diff --git a/drizzle/mysql/meta/0038_snapshot.json b/drizzle/mysql/meta/0038_snapshot.json new file mode 100644 index 000000000..443c311af --- /dev/null +++ b/drizzle/mysql/meta/0038_snapshot.json @@ -0,0 +1,8513 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "50a2201a-6d8c-4417-a939-1a701658c68b", + "prevId": "95cda11f-9a59-445a-bcd1-b106ce8adcec", + "tables": { + "ai_conversations": { + "name": "ai_conversations", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ai_conversations_user": { + "name": "idx_ai_conversations_user", + "columns": [ + "user_id", + "updated_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ai_conversations_user_id_users_id_fk": { + "name": "ai_conversations_user_id_users_id_fk", + "tableFrom": "ai_conversations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ai_conversations_id": { + "name": "ai_conversations_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ai_messages": { + "name": "ai_messages", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('')" + }, + "tool_calls": { + "name": "tool_calls", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ai_messages_conversation": { + "name": "idx_ai_messages_conversation", + "columns": [ + "conversation_id", + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ai_messages_conversation_id_ai_conversations_id_fk": { + "name": "ai_messages_conversation_id_ai_conversations_id_fk", + "tableFrom": "ai_messages", + "tableTo": "ai_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ai_messages_id": { + "name": "ai_messages_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ai_proposals": { + "name": "ai_proposals", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "status": { + "name": "status", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "result_summary": { + "name": "result_summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ai_proposals_user": { + "name": "idx_ai_proposals_user", + "columns": [ + "user_id", + "status" + ], + "isUnique": false + }, + "idx_ai_proposals_conversation": { + "name": "idx_ai_proposals_conversation", + "columns": [ + "conversation_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ai_proposals_conversation_id_ai_conversations_id_fk": { + "name": "ai_proposals_conversation_id_ai_conversations_id_fk", + "tableFrom": "ai_proposals", + "tableTo": "ai_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ai_proposals_user_id_users_id_fk": { + "name": "ai_proposals_user_id_users_id_fk", + "tableFrom": "ai_proposals", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ai_proposals_id": { + "name": "ai_proposals_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ai_providers": { + "name": "ai_providers", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_type": { + "name": "provider_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "api_key": { + "name": "api_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "api_key_prefix": { + "name": "api_key_prefix", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_model": { + "name": "default_model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ai_providers_user_label": { + "name": "idx_ai_providers_user_label", + "columns": [ + "user_id", + "label" + ], + "isUnique": true + } + }, + "foreignKeys": { + "ai_providers_user_id_users_id_fk": { + "name": "ai_providers_user_id_users_id_fk", + "tableFrom": "ai_providers", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ai_providers_id": { + "name": "ai_providers_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "api_keys": { + "name": "api_keys", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": { + "idx_api_keys_user_id": { + "name": "idx_api_keys_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "api_keys_id": { + "name": "api_keys_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_audit_logs_timestamp": { + "name": "idx_audit_logs_timestamp", + "columns": [ + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_user_ts": { + "name": "idx_audit_logs_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_action_ts": { + "name": "idx_audit_logs_action_ts", + "columns": [ + "action", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_resource_ts": { + "name": "idx_audit_logs_resource_ts", + "columns": [ + "resource_type", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "audit_logs_id": { + "name": "audit_logs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "automation_channels": { + "name": "automation_channels", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "automation_id": { + "name": "automation_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel_id": { + "name": "channel_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_automation_channels_pair": { + "name": "idx_automation_channels_pair", + "columns": [ + "automation_id", + "channel_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "automation_channels_automation_id_automations_id_fk": { + "name": "automation_channels_automation_id_automations_id_fk", + "tableFrom": "automation_channels", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_channels_channel_id_notification_channels_id_fk": { + "name": "automation_channels_channel_id_notification_channels_id_fk", + "tableFrom": "automation_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_channels_id": { + "name": "automation_channels_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "automation_run_steps": { + "name": "automation_run_steps", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "run_id": { + "name": "run_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "step_index": { + "name": "step_index", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "step_id": { + "name": "step_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "step_type": { + "name": "step_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "finished_at": { + "name": "finished_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "output": { + "name": "output", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "truncated": { + "name": "truncated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "idx_automation_run_steps_run": { + "name": "idx_automation_run_steps_run", + "columns": [ + "run_id", + "step_index" + ], + "isUnique": false + } + }, + "foreignKeys": { + "automation_run_steps_run_id_automation_runs_id_fk": { + "name": "automation_run_steps_run_id_automation_runs_id_fk", + "tableFrom": "automation_run_steps", + "tableTo": "automation_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_run_steps_id": { + "name": "automation_run_steps_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "automation_runs": { + "name": "automation_runs", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "automation_id": { + "name": "automation_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "trigger_context": { + "name": "trigger_context", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "finished_at": { + "name": "finished_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dry_run": { + "name": "dry_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_automation_runs_automation": { + "name": "idx_automation_runs_automation", + "columns": [ + "automation_id", + "started_at" + ], + "isUnique": false + }, + "idx_automation_runs_user": { + "name": "idx_automation_runs_user", + "columns": [ + "user_id", + "started_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "automation_runs_automation_id_automations_id_fk": { + "name": "automation_runs_automation_id_automations_id_fk", + "tableFrom": "automation_runs", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_runs_user_id_users_id_fk": { + "name": "automation_runs_user_id_users_id_fk", + "tableFrom": "automation_runs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_runs_id": { + "name": "automation_runs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "automation_schedules": { + "name": "automation_schedules", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "automation_id": { + "name": "automation_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cron": { + "name": "cron", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "next_due_at": { + "name": "next_due_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_tick_at": { + "name": "last_tick_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_automation_schedules_automation": { + "name": "idx_automation_schedules_automation", + "columns": [ + "automation_id" + ], + "isUnique": true + }, + "idx_automation_schedules_due": { + "name": "idx_automation_schedules_due", + "columns": [ + "next_due_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "automation_schedules_automation_id_automations_id_fk": { + "name": "automation_schedules_automation_id_automations_id_fk", + "tableFrom": "automation_schedules", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_schedules_id": { + "name": "automation_schedules_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "automation_trigger_state": { + "name": "automation_trigger_state", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "automation_id": { + "name": "automation_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "state_key": { + "name": "state_key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "breach_started_at": { + "name": "breach_started_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_value": { + "name": "last_value", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_observed_state": { + "name": "last_observed_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_automation_trigger_state_key": { + "name": "idx_automation_trigger_state_key", + "columns": [ + "automation_id", + "state_key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "automation_trigger_state_automation_id_automations_id_fk": { + "name": "automation_trigger_state_automation_id_automations_id_fk", + "tableFrom": "automation_trigger_state", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_trigger_state_id": { + "name": "automation_trigger_state_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "automations": { + "name": "automations", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "definition": { + "name": "definition", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "definition_version": { + "name": "definition_version", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "concurrency_policy": { + "name": "concurrency_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('skip')" + }, + "max_run_seconds": { + "name": "max_run_seconds", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 300 + }, + "dry_run": { + "name": "dry_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "last_run_at": { + "name": "last_run_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_run_status": { + "name": "last_run_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_automations_user": { + "name": "idx_automations_user", + "columns": [ + "user_id", + "enabled" + ], + "isUnique": false + } + }, + "foreignKeys": { + "automations_user_id_users_id_fk": { + "name": "automations_user_id_users_id_fk", + "tableFrom": "automations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automations_id": { + "name": "automations_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "collab_room_members": { + "name": "collab_room_members", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "room_id": { + "name": "room_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "room_role": { + "name": "room_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('member')" + }, + "added_by": { + "name": "added_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_collab_room_members_room_user": { + "name": "idx_collab_room_members_room_user", + "columns": [ + "room_id", + "user_id" + ], + "isUnique": true + }, + "idx_collab_room_members_user": { + "name": "idx_collab_room_members_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "collab_room_members_room_id_collab_rooms_id_fk": { + "name": "collab_room_members_room_id_collab_rooms_id_fk", + "tableFrom": "collab_room_members", + "tableTo": "collab_rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "collab_room_members_user_id_users_id_fk": { + "name": "collab_room_members_user_id_users_id_fk", + "tableFrom": "collab_room_members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "collab_room_members_added_by_users_id_fk": { + "name": "collab_room_members_added_by_users_id_fk", + "tableFrom": "collab_room_members", + "tableTo": "users", + "columnsFrom": [ + "added_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "collab_room_members_id": { + "name": "collab_room_members_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "collab_rooms": { + "name": "collab_rooms", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "persistent": { + "name": "persistent", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "presenter_user_id": { + "name": "presenter_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stage_protocol": { + "name": "stage_protocol", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stage_host_id": { + "name": "stage_host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stage_share_id": { + "name": "stage_share_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "guest_link_token": { + "name": "guest_link_token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "ended_at": { + "name": "ended_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_collab_rooms_owner": { + "name": "idx_collab_rooms_owner", + "columns": [ + "owner_user_id" + ], + "isUnique": false + }, + "idx_collab_rooms_guest_token": { + "name": "idx_collab_rooms_guest_token", + "columns": [ + "guest_link_token" + ], + "isUnique": true + } + }, + "foreignKeys": { + "collab_rooms_owner_user_id_users_id_fk": { + "name": "collab_rooms_owner_user_id_users_id_fk", + "tableFrom": "collab_rooms", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "collab_rooms_presenter_user_id_users_id_fk": { + "name": "collab_rooms_presenter_user_id_users_id_fk", + "tableFrom": "collab_rooms", + "tableTo": "users", + "columnsFrom": [ + "presenter_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "collab_rooms_stage_host_id_ssh_data_id_fk": { + "name": "collab_rooms_stage_host_id_ssh_data_id_fk", + "tableFrom": "collab_rooms", + "tableTo": "ssh_data", + "columnsFrom": [ + "stage_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "collab_rooms_stage_share_id_session_shares_id_fk": { + "name": "collab_rooms_stage_share_id_session_shares_id_fk", + "tableFrom": "collab_rooms", + "tableTo": "session_shares", + "columnsFrom": [ + "stage_share_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "collab_rooms_id": { + "name": "collab_rooms_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "credential_access": { + "name": "credential_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('use')" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_credential_access_user_id": { + "name": "idx_credential_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_credential_access_role_id": { + "name": "idx_credential_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + }, + "idx_credential_access_credential_id": { + "name": "idx_credential_access_credential_id", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "credential_access_credential_id_ssh_credentials_id_fk": { + "name": "credential_access_credential_id_ssh_credentials_id_fk", + "tableFrom": "credential_access", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_user_id_users_id_fk": { + "name": "credential_access_user_id_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_role_id_roles_id_fk": { + "name": "credential_access_role_id_roles_id_fk", + "tableFrom": "credential_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_granted_by_users_id_fk": { + "name": "credential_access_granted_by_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "credential_access_id": { + "name": "credential_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "credential_sidebar_preferences_user_id": { + "name": "credential_sidebar_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "dashboard_service_links": { + "name": "dashboard_service_links", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "dashboard_service_links_user_id_users_id_fk": { + "name": "dashboard_service_links_user_id_users_id_fk", + "tableFrom": "dashboard_service_links", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "dashboard_service_links_id": { + "name": "dashboard_service_links_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "dashboard_service_links_sync_id_unique": { + "name": "dashboard_service_links_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "dismissed_alerts": { + "name": "dismissed_alerts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_dismissed_alerts_user_id": { + "name": "idx_dismissed_alerts_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "dismissed_alerts_id": { + "name": "dismissed_alerts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "folder_access": { + "name": "folder_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('connect')" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_folder_access_owner_folder": { + "name": "idx_folder_access_owner_folder", + "columns": [ + "owner_user_id", + "folder" + ], + "isUnique": false + } + }, + "foreignKeys": { + "folder_access_owner_user_id_users_id_fk": { + "name": "folder_access_owner_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_user_id_users_id_fk": { + "name": "folder_access_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_role_id_roles_id_fk": { + "name": "folder_access_role_id_roles_id_fk", + "tableFrom": "folder_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_granted_by_users_id_fk": { + "name": "folder_access_granted_by_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "folder_access_id": { + "name": "folder_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "homepage_items": { + "name": "homepage_items", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "folder_id": { + "name": "folder_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_homepage_items_user_id": { + "name": "idx_homepage_items_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "homepage_items_user_id_users_id_fk": { + "name": "homepage_items_user_id_users_id_fk", + "tableFrom": "homepage_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "homepage_items_id": { + "name": "homepage_items_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "homepage_items_sync_id_unique": { + "name": "homepage_items_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "homepage_layouts": { + "name": "homepage_layouts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_layouts_user_id_users_id_fk": { + "name": "homepage_layouts_user_id_users_id_fk", + "tableFrom": "homepage_layouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "homepage_layouts_id": { + "name": "homepage_layouts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "homepage_layouts_user_id_unique": { + "name": "homepage_layouts_user_id_unique", + "columns": [ + "user_id" + ] + } + }, + "checkConstraint": {} + }, + "host_access": { + "name": "host_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('connect')" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_count": { + "name": "access_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "idx_host_access_user_id": { + "name": "idx_host_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_host_access_role_id": { + "name": "idx_host_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + }, + "idx_host_access_host_id": { + "name": "idx_host_access_host_id", + "columns": [ + "host_id" + ], + "isUnique": false + }, + "idx_host_access_expires_at": { + "name": "idx_host_access_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_access_id": { + "name": "host_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_health_checks": { + "name": "host_health_checks", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checks": { + "name": "checks", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 300 + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_host_health_checks_user_host": { + "name": "idx_host_health_checks_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_health_checks_user_id_users_id_fk": { + "name": "host_health_checks_user_id_users_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_checks_host_id_ssh_data_id_fk": { + "name": "host_health_checks_host_id_ssh_data_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_health_checks_id": { + "name": "host_health_checks_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_health_history": { + "name": "host_health_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "ok": { + "name": "ok", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_health_history_user_id_users_id_fk": { + "name": "host_health_history_user_id_users_id_fk", + "tableFrom": "host_health_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_history_host_id_ssh_data_id_fk": { + "name": "host_health_history_host_id_ssh_data_id_fk", + "tableFrom": "host_health_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_health_history_id": { + "name": "host_health_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_metrics_history": { + "name": "host_metrics_history", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_metrics_history_host_id_ssh_data_id_fk": { + "name": "host_metrics_history_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_metrics_history_id": { + "name": "host_metrics_history_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_metrics_preferences": { + "name": "host_metrics_preferences", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_host_metrics_prefs_user_host": { + "name": "idx_host_metrics_prefs_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_metrics_preferences_user_id_users_id_fk": { + "name": "host_metrics_preferences_user_id_users_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_metrics_preferences_host_id_ssh_data_id_fk": { + "name": "host_metrics_preferences_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_metrics_preferences_id": { + "name": "host_metrics_preferences_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_sidebar_preferences_user_id": { + "name": "host_sidebar_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_data": { + "name": "ssh_data", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('ssh')" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_host_id": { + "name": "parent_host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_docker": { + "name": "enable_docker", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_web_ui": { + "name": "enable_web_ui", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_terminal_toolbar": { + "name": "enable_terminal_toolbar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_ai_assistant": { + "name": "enable_ai_assistant", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stats_config": { + "name": "stats_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "docker_config": { + "name": "docker_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "web_ui_config": { + "name": "web_ui_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_rdp": { + "name": "enable_rdp", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_vnc": { + "name": "enable_vnc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_telnet": { + "name": "enable_telnet", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "ssh_port": { + "name": "ssh_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "rdp_port": { + "name": "rdp_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 3389 + }, + "vnc_port": { + "name": "vnc_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 5900 + }, + "telnet_port": { + "name": "telnet_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 23 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_security": { + "name": "rdp_security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_ignore_cert": { + "name": "rdp_ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "security": { + "name": "security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ignore_cert": { + "name": "ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "guacamole_config": { + "name": "guacamole_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "wol_broadcast_address": { + "name": "wol_broadcast_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('sha256')" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_data_user_id": { + "name": "idx_ssh_data_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_ssh_data_parent_host": { + "name": "idx_ssh_data_parent_host", + "columns": [ + "parent_host_id" + ], + "isUnique": false + }, + "idx_ssh_data_credential": { + "name": "idx_ssh_data_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_parent_host_id_ssh_data_id_fk": { + "name": "ssh_data_parent_host_id_ssh_data_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_data", + "columnsFrom": [ + "parent_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_data_id": { + "name": "ssh_data_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "notification_channels": { + "name": "notification_channels", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "notification_channels_id": { + "name": "notification_channels_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "opkssh_tokens": { + "name": "opkssh_tokens", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "opkssh_tokens_id": { + "name": "opkssh_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_install_counts": { + "name": "plugin_install_counts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "registry_id": { + "name": "registry_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('aggregate-telemetry')" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_plugin_install_counts_plugin_registry": { + "name": "idx_plugin_install_counts_plugin_registry", + "columns": [ + "plugin_id", + "registry_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "plugin_install_counts_id": { + "name": "plugin_install_counts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_migrations": { + "name": "plugin_migrations", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "migration_id": { + "name": "migration_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_plugin_migrations_plugin_migration": { + "name": "idx_plugin_migrations_plugin_migration", + "columns": [ + "plugin_id", + "migration_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_migrations_plugin_id_plugins_id_fk": { + "name": "plugin_migrations_plugin_id_plugins_id_fk", + "tableFrom": "plugin_migrations", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "plugin_migrations_id": { + "name": "plugin_migrations_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_permission_grants": { + "name": "plugin_permission_grants", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "capability": { + "name": "capability", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('admin')" + } + }, + "indexes": { + "idx_plugin_permission_grants_plugin_capability": { + "name": "idx_plugin_permission_grants_plugin_capability", + "columns": [ + "plugin_id", + "capability" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_permission_grants_plugin_id_plugins_id_fk": { + "name": "plugin_permission_grants_plugin_id_plugins_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_permission_grants_granted_by_users_id_fk": { + "name": "plugin_permission_grants_granted_by_users_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "plugin_permission_grants_id": { + "name": "plugin_permission_grants_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_registries": { + "name": "plugin_registries", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('community')" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "signing_key": { + "name": "signing_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_checked_at": { + "name": "last_checked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_index_hash": { + "name": "last_index_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "plugin_registries_id": { + "name": "plugin_registries_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_settings": { + "name": "plugin_settings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted": { + "name": "encrypted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_plugin_settings_scope_key": { + "name": "idx_plugin_settings_scope_key", + "columns": [ + "plugin_id", + "scope", + "scope_id", + "key" + ], + "isUnique": true + }, + "idx_plugin_settings_plugin_scope": { + "name": "idx_plugin_settings_plugin_scope", + "columns": [ + "plugin_id", + "scope" + ], + "isUnique": false + } + }, + "foreignKeys": { + "plugin_settings_plugin_id_plugins_id_fk": { + "name": "plugin_settings_plugin_id_plugins_id_fk", + "tableFrom": "plugin_settings", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "plugin_settings_id": { + "name": "plugin_settings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_storage": { + "name": "plugin_storage", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_plugin_storage_plugin_key": { + "name": "idx_plugin_storage_plugin_key", + "columns": [ + "plugin_id", + "key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_storage_plugin_id_plugins_id_fk": { + "name": "plugin_storage_plugin_id_plugins_id_fk", + "tableFrom": "plugin_storage", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "plugin_storage_id": { + "name": "plugin_storage_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugins": { + "name": "plugins", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('available')" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('community')" + }, + "registry_id": { + "name": "registry_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('disabled')" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "installed_at": { + "name": "installed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "auto_update": { + "name": "auto_update", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "manifest_json": { + "name": "manifest_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_plugins_registry_id": { + "name": "idx_plugins_registry_id", + "columns": [ + "registry_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "plugins_id": { + "name": "plugins_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "rbac_applied_defaults": { + "name": "rbac_applied_defaults", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "role_name": { + "name": "role_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_rbac_applied_defaults_role_permission": { + "name": "idx_rbac_applied_defaults_role_permission", + "columns": [ + "role_name", + "permission" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "rbac_applied_defaults_id": { + "name": "rbac_applied_defaults_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "rbac_known_permissions": { + "name": "rbac_known_permissions", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "permission": { + "name": "permission", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_rbac_known_permissions_permission": { + "name": "idx_rbac_known_permissions_permission", + "columns": [ + "permission" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "rbac_known_permissions_id": { + "name": "rbac_known_permissions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "recent_activity": { + "name": "recent_activity", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_recent_activity_user_ts": { + "name": "idx_recent_activity_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "recent_activity_id": { + "name": "recent_activity_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "roles_id": { + "name": "roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ] + } + }, + "checkConstraint": {} + }, + "secret_sources": { + "name": "secret_sources", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('onepassword-connect')" + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "shared": { + "name": "shared", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_secret_sources_user": { + "name": "idx_secret_sources_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "secret_sources_user_id_users_id_fk": { + "name": "secret_sources_user_id_users_id_fk", + "tableFrom": "secret_sources", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "secret_sources_id": { + "name": "secret_sources_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "session_recordings": { + "name": "session_recordings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_id": { + "name": "access_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "ended_at": { + "name": "ended_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration": { + "name": "duration", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "commands": { + "name": "commands", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dangerous_actions": { + "name": "dangerous_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recording_path": { + "name": "recording_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('text')" + }, + "terminated_by_owner": { + "name": "terminated_by_owner", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "termination_reason": { + "name": "termination_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_session_recordings_user_started": { + "name": "idx_session_recordings_user_started", + "columns": [ + "user_id", + "started_at" + ], + "isUnique": false + }, + "idx_session_recordings_host": { + "name": "idx_session_recordings_host", + "columns": [ + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "session_recordings_host_id_ssh_data_id_fk": { + "name": "session_recordings_host_id_ssh_data_id_fk", + "tableFrom": "session_recordings", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_recordings_user_id_users_id_fk": { + "name": "session_recordings_user_id_users_id_fk", + "tableFrom": "session_recordings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "session_recordings_access_id_host_access_id_fk": { + "name": "session_recordings_access_id_host_access_id_fk", + "tableFrom": "session_recordings", + "tableTo": "host_access", + "columnsFrom": [ + "access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_recordings_id": { + "name": "session_recordings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "session_share_participants": { + "name": "session_share_participants", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "share_id": { + "name": "share_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "guest_label": { + "name": "guest_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "joined_at": { + "name": "joined_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "left_at": { + "name": "left_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_share_participants_share_id_session_shares_id_fk": { + "name": "session_share_participants_share_id_session_shares_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "session_shares", + "columnsFrom": [ + "share_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_share_participants_user_id_users_id_fk": { + "name": "session_share_participants_user_id_users_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_share_participants_id": { + "name": "session_share_participants_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "session_shares": { + "name": "session_shares", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_instance_id": { + "name": "tab_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "share_type": { + "name": "share_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "link_token": { + "name": "link_token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('read-only')" + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_joined_at": { + "name": "last_joined_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "join_count": { + "name": "join_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "idx_session_shares_session_id": { + "name": "idx_session_shares_session_id", + "columns": [ + "session_id" + ], + "isUnique": false + }, + "idx_session_shares_host_id": { + "name": "idx_session_shares_host_id", + "columns": [ + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "session_shares_host_id_ssh_data_id_fk": { + "name": "session_shares_host_id_ssh_data_id_fk", + "tableFrom": "session_shares", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_owner_user_id_users_id_fk": { + "name": "session_shares_owner_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_target_user_id_users_id_fk": { + "name": "session_shares_target_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_shares_id": { + "name": "session_shares_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "session_shares_link_token_unique": { + "name": "session_shares_link_token_unique", + "columns": [ + "link_token" + ] + } + }, + "checkConstraint": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_sessions_user_id": { + "name": "idx_sessions_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_sessions_expires_at": { + "name": "idx_sessions_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sessions_id": { + "name": "sessions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "settings_key": { + "name": "settings_key", + "columns": [ + "key" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_credential_secrets": { + "name": "shared_credential_secrets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "credential_access_id": { + "name": "credential_access_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('password')" + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_shared_credential_secrets_scope": { + "name": "idx_shared_credential_secrets_scope", + "columns": [ + "credential_access_id", + "target_user_id" + ], + "isUnique": true + }, + "idx_shared_credential_secrets_target": { + "name": "idx_shared_credential_secrets_target", + "columns": [ + "target_user_id", + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "shared_credential_secrets_target_user_id_users_id_fk": { + "name": "shared_credential_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_credential_secrets_credential_id_ssh_credentials_id_fk": { + "name": "shared_credential_secrets_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_cred_secrets_access_id_fk": { + "name": "shared_cred_secrets_access_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "credential_access", + "columnsFrom": [ + "credential_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_credential_secrets_id": { + "name": "shared_credential_secrets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + "host_id", + "user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_host_auth_overrides_id": { + "name": "shared_host_auth_overrides_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_host_secrets": { + "name": "shared_host_secrets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('credential')" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + "host_access_id", + "target_user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_host_secrets_id": { + "name": "shared_host_secrets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_credential_usage": { + "name": "ssh_credential_usage", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_credential_usage_credential": { + "name": "idx_ssh_credential_usage_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + }, + "idx_ssh_credential_usage_user": { + "name": "idx_ssh_credential_usage_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_credential_usage_id": { + "name": "ssh_credential_usage_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_credentials": { + "name": "ssh_credentials", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "usage_count": { + "name": "usage_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_credentials_user_id": { + "name": "idx_ssh_credentials_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_credentials_id": { + "name": "ssh_credentials_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "ssh_folders": { + "name": "ssh_folders", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_folders_user_id": { + "name": "idx_ssh_folders_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_folders_id": { + "name": "ssh_folders_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "sso_providers": { + "name": "sso_providers", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "sso_providers_id": { + "name": "sso_providers_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "sync_tombstones": { + "name": "sync_tombstones", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sync_tombstones_id": { + "name": "sync_tombstones_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "termix_identities": { + "name": "termix_identities", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "handle": { + "name": "handle", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identities_id": { + "name": "termix_identities_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "columns": [ + "user_id" + ] + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "columns": [ + "handle" + ] + } + }, + "checkConstraint": {} + }, + "termix_identity_ca": { + "name": "termix_identity_ca", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "validity_days": { + "name": "validity_days", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identity_ca_id": { + "name": "termix_identity_ca_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "columns": [ + "identity_id" + ] + } + }, + "checkConstraint": {} + }, + "termix_identity_keys": { + "name": "termix_identity_keys", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('manual')" + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identity_keys_id": { + "name": "termix_identity_keys_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "tmux_session_tags": { + "name": "tmux_session_tags", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_name": { + "name": "session_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "tmux_session_tags_user_id_users_id_fk": { + "name": "tmux_session_tags_user_id_users_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tmux_session_tags_host_id_ssh_data_id_fk": { + "name": "tmux_session_tags_host_id_ssh_data_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "tmux_session_tags_id": { + "name": "tmux_session_tags_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "trusted_devices": { + "name": "trusted_devices", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_trusted_devices_user_id": { + "name": "idx_trusted_devices_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "trusted_devices_id": { + "name": "trusted_devices_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ui_preferences": { + "name": "ui_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ui_preferences_user_id_users_id_fk": { + "name": "ui_preferences_user_id_users_id_fk", + "tableFrom": "ui_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ui_preferences_user_id": { + "name": "ui_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_external_identities": { + "name": "user_external_identities", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_external_identities_provider_subject": { + "name": "idx_user_external_identities_provider_subject", + "columns": [ + "provider_id", + "subject" + ], + "isUnique": true + }, + "idx_user_external_identities_user": { + "name": "idx_user_external_identities_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_external_identities_user_id_users_id_fk": { + "name": "user_external_identities_user_id_users_id_fk", + "tableFrom": "user_external_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_external_identities_id": { + "name": "user_external_identities_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_open_tabs": { + "name": "user_open_tabs", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_open_tabs_user_id": { + "name": "idx_user_open_tabs_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_open_tabs_id": { + "name": "user_open_tabs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_pin_app_rail_button": { + "name": "show_pin_app_rail_button", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ai_assistant_enabled": { + "name": "ai_assistant_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ai_read_only_commands": { + "name": "ai_read_only_commands", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_defaults": { + "name": "terminal_defaults", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_defaults": { + "name": "rdp_defaults", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_macros": { + "name": "terminal_macros", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_preferences_user_id": { + "name": "user_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + "user_id", + "role_id" + ], + "isUnique": true + }, + "idx_user_roles_role_id": { + "name": "idx_user_roles_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_roles_id": { + "name": "user_roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_second_factors": { + "name": "user_second_factors", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "factor_id": { + "name": "factor_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enrolled_at": { + "name": "enrolled_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_second_factors_user_factor": { + "name": "idx_user_second_factors_user_factor", + "columns": [ + "user_id", + "plugin_id", + "factor_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_second_factors_user_id_users_id_fk": { + "name": "user_second_factors_user_id_users_id_fk", + "tableFrom": "user_second_factors", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_second_factors_id": { + "name": "user_second_factors_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('openid email profile')" + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totp_enabled": { + "name": "totp_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "totp_backup_codes": { + "name": "totp_backup_codes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "users_id": { + "name": "users_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "vault_profiles": { + "name": "vault_profiles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shared": { + "name": "shared", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "vault_profiles_id": { + "name": "vault_profiles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "vault_tokens": { + "name": "vault_tokens", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "profile_id": { + "name": "profile_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + "user_id", + "profile_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "vault_tokens_id": { + "name": "vault_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "webauthn_credentials": { + "name": "webauthn_credentials", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_verification": { + "name": "user_verification", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('preferred')" + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "webauthn_credentials_user_id_users_id_fk": { + "name": "webauthn_credentials_user_id_users_id_fk", + "tableFrom": "webauthn_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "webauthn_credentials_id": { + "name": "webauthn_credentials_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/mysql/meta/_journal.json b/drizzle/mysql/meta/_journal.json index 0b2919ff8..1f8e2d116 100644 --- a/drizzle/mysql/meta/_journal.json +++ b/drizzle/mysql/meta/_journal.json @@ -267,6 +267,13 @@ "when": 1790201005569, "tag": "0037_foamy_norman_osborn", "breakpoints": true + }, + { + "idx": 38, + "version": "5", + "when": 1790208810478, + "tag": "0038_quiet_karnak", + "breakpoints": true } ] } \ No newline at end of file diff --git a/drizzle/postgres/0039_violet_firedrake.sql b/drizzle/postgres/0039_violet_firedrake.sql new file mode 100644 index 000000000..c6b92fab6 --- /dev/null +++ b/drizzle/postgres/0039_violet_firedrake.sql @@ -0,0 +1,5 @@ +-- command_history moved to the ssh-terminal plugin, whose first migration +-- renames it to p_ssh_terminal_command_history. Dropping it here would lose +-- every saved command, so this only stops drizzle tracking it. SELECT 1 +-- because MySQL rejects an empty query. +SELECT 1; diff --git a/drizzle/postgres/meta/0039_snapshot.json b/drizzle/postgres/meta/0039_snapshot.json new file mode 100644 index 000000000..916ddda00 --- /dev/null +++ b/drizzle/postgres/meta/0039_snapshot.json @@ -0,0 +1,8309 @@ +{ + "id": "fc97518e-ee51-4154-a4f9-d68da509c6c1", + "prevId": "062b236b-4538-4009-b895-01f157c52828", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.ai_conversations": { + "name": "ai_conversations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_conversations_user": { + "name": "idx_ai_conversations_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_conversations_user_id_users_id_fk": { + "name": "ai_conversations_user_id_users_id_fk", + "tableFrom": "ai_conversations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_messages": { + "name": "ai_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "tool_calls": { + "name": "tool_calls", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_messages_conversation": { + "name": "idx_ai_messages_conversation", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_messages_conversation_id_ai_conversations_id_fk": { + "name": "ai_messages_conversation_id_ai_conversations_id_fk", + "tableFrom": "ai_messages", + "tableTo": "ai_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_proposals": { + "name": "ai_proposals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "status": { + "name": "status", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_summary": { + "name": "result_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_proposals_user": { + "name": "idx_ai_proposals_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ai_proposals_conversation": { + "name": "idx_ai_proposals_conversation", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_proposals_conversation_id_ai_conversations_id_fk": { + "name": "ai_proposals_conversation_id_ai_conversations_id_fk", + "tableFrom": "ai_proposals", + "tableTo": "ai_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ai_proposals_user_id_users_id_fk": { + "name": "ai_proposals_user_id_users_id_fk", + "tableFrom": "ai_proposals", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_providers": { + "name": "ai_providers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "provider_type": { + "name": "provider_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_key": { + "name": "api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_key_prefix": { + "name": "api_key_prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_model": { + "name": "default_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_providers_user_label": { + "name": "idx_ai_providers_user_label", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "label", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_providers_user_id_users_id_fk": { + "name": "ai_providers_user_id_users_id_fk", + "tableFrom": "ai_providers", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "idx_api_keys_user_id": { + "name": "idx_api_keys_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_logs": { + "name": "audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_audit_logs_timestamp": { + "name": "idx_audit_logs_timestamp", + "columns": [ + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_logs_user_ts": { + "name": "idx_audit_logs_user_ts", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_logs_action_ts": { + "name": "idx_audit_logs_action_ts", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_logs_resource_ts": { + "name": "idx_audit_logs_resource_ts", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_channels": { + "name": "automation_channels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "channel_id": { + "name": "channel_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_automation_channels_pair": { + "name": "idx_automation_channels_pair", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "channel_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_channels_automation_id_automations_id_fk": { + "name": "automation_channels_automation_id_automations_id_fk", + "tableFrom": "automation_channels", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_channels_channel_id_notification_channels_id_fk": { + "name": "automation_channels_channel_id_notification_channels_id_fk", + "tableFrom": "automation_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_run_steps": { + "name": "automation_run_steps", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "step_index": { + "name": "step_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "step_id": { + "name": "step_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "step_type": { + "name": "step_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "finished_at": { + "name": "finished_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "truncated": { + "name": "truncated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "idx_automation_run_steps_run": { + "name": "idx_automation_run_steps_run", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "step_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_run_steps_run_id_automation_runs_id_fk": { + "name": "automation_run_steps_run_id_automation_runs_id_fk", + "tableFrom": "automation_run_steps", + "tableTo": "automation_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_runs": { + "name": "automation_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trigger_context": { + "name": "trigger_context", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "finished_at": { + "name": "finished_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dry_run": { + "name": "dry_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_automation_runs_automation": { + "name": "idx_automation_runs_automation", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_automation_runs_user": { + "name": "idx_automation_runs_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_runs_automation_id_automations_id_fk": { + "name": "automation_runs_automation_id_automations_id_fk", + "tableFrom": "automation_runs", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_runs_user_id_users_id_fk": { + "name": "automation_runs_user_id_users_id_fk", + "tableFrom": "automation_runs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_schedules": { + "name": "automation_schedules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "cron": { + "name": "cron", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_due_at": { + "name": "next_due_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "last_tick_at": { + "name": "last_tick_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_automation_schedules_automation": { + "name": "idx_automation_schedules_automation", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_automation_schedules_due": { + "name": "idx_automation_schedules_due", + "columns": [ + { + "expression": "next_due_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_schedules_automation_id_automations_id_fk": { + "name": "automation_schedules_automation_id_automations_id_fk", + "tableFrom": "automation_schedules", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_trigger_state": { + "name": "automation_trigger_state", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "state_key": { + "name": "state_key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "breach_started_at": { + "name": "breach_started_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_value": { + "name": "last_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "last_observed_state": { + "name": "last_observed_state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_automation_trigger_state_key": { + "name": "idx_automation_trigger_state_key", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_trigger_state_automation_id_automations_id_fk": { + "name": "automation_trigger_state_automation_id_automations_id_fk", + "tableFrom": "automation_trigger_state", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automations": { + "name": "automations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "definition": { + "name": "definition", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "definition_version": { + "name": "definition_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "concurrency_policy": { + "name": "concurrency_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'skip'" + }, + "max_run_seconds": { + "name": "max_run_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 300 + }, + "dry_run": { + "name": "dry_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "last_run_at": { + "name": "last_run_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_run_status": { + "name": "last_run_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_automations_user": { + "name": "idx_automations_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automations_user_id_users_id_fk": { + "name": "automations_user_id_users_id_fk", + "tableFrom": "automations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.collab_room_members": { + "name": "collab_room_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "room_id": { + "name": "room_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "room_role": { + "name": "room_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "added_by": { + "name": "added_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_collab_room_members_room_user": { + "name": "idx_collab_room_members_room_user", + "columns": [ + { + "expression": "room_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_collab_room_members_user": { + "name": "idx_collab_room_members_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "collab_room_members_room_id_collab_rooms_id_fk": { + "name": "collab_room_members_room_id_collab_rooms_id_fk", + "tableFrom": "collab_room_members", + "tableTo": "collab_rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "collab_room_members_user_id_users_id_fk": { + "name": "collab_room_members_user_id_users_id_fk", + "tableFrom": "collab_room_members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "collab_room_members_added_by_users_id_fk": { + "name": "collab_room_members_added_by_users_id_fk", + "tableFrom": "collab_room_members", + "tableTo": "users", + "columnsFrom": [ + "added_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.collab_rooms": { + "name": "collab_rooms", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "persistent": { + "name": "persistent", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "presenter_user_id": { + "name": "presenter_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "stage_protocol": { + "name": "stage_protocol", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stage_host_id": { + "name": "stage_host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "stage_share_id": { + "name": "stage_share_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "guest_link_token": { + "name": "guest_link_token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "ended_at": { + "name": "ended_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_collab_rooms_owner": { + "name": "idx_collab_rooms_owner", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_collab_rooms_guest_token": { + "name": "idx_collab_rooms_guest_token", + "columns": [ + { + "expression": "guest_link_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "collab_rooms_owner_user_id_users_id_fk": { + "name": "collab_rooms_owner_user_id_users_id_fk", + "tableFrom": "collab_rooms", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "collab_rooms_presenter_user_id_users_id_fk": { + "name": "collab_rooms_presenter_user_id_users_id_fk", + "tableFrom": "collab_rooms", + "tableTo": "users", + "columnsFrom": [ + "presenter_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "collab_rooms_stage_host_id_ssh_data_id_fk": { + "name": "collab_rooms_stage_host_id_ssh_data_id_fk", + "tableFrom": "collab_rooms", + "tableTo": "ssh_data", + "columnsFrom": [ + "stage_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "collab_rooms_stage_share_id_session_shares_id_fk": { + "name": "collab_rooms_stage_share_id_session_shares_id_fk", + "tableFrom": "collab_rooms", + "tableTo": "session_shares", + "columnsFrom": [ + "stage_share_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_access": { + "name": "credential_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'use'" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_credential_access_user_id": { + "name": "idx_credential_access_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_credential_access_role_id": { + "name": "idx_credential_access_role_id", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_credential_access_credential_id": { + "name": "idx_credential_access_credential_id", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_access_credential_id_ssh_credentials_id_fk": { + "name": "credential_access_credential_id_ssh_credentials_id_fk", + "tableFrom": "credential_access", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_user_id_users_id_fk": { + "name": "credential_access_user_id_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_role_id_roles_id_fk": { + "name": "credential_access_role_id_roles_id_fk", + "tableFrom": "credential_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_granted_by_users_id_fk": { + "name": "credential_access_granted_by_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dashboard_service_links": { + "name": "dashboard_service_links", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "dashboard_service_links_user_id_users_id_fk": { + "name": "dashboard_service_links_user_id_users_id_fk", + "tableFrom": "dashboard_service_links", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "dashboard_service_links_sync_id_unique": { + "name": "dashboard_service_links_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dismissed_alerts": { + "name": "dismissed_alerts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_dismissed_alerts_user_id": { + "name": "idx_dismissed_alerts_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.folder_access": { + "name": "folder_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "folder": { + "name": "folder", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_folder_access_owner_folder": { + "name": "idx_folder_access_owner_folder", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "folder", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "folder_access_owner_user_id_users_id_fk": { + "name": "folder_access_owner_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_user_id_users_id_fk": { + "name": "folder_access_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_role_id_roles_id_fk": { + "name": "folder_access_role_id_roles_id_fk", + "tableFrom": "folder_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_granted_by_users_id_fk": { + "name": "folder_access_granted_by_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.homepage_items": { + "name": "homepage_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "folder_id": { + "name": "folder_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_homepage_items_user_id": { + "name": "idx_homepage_items_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "homepage_items_user_id_users_id_fk": { + "name": "homepage_items_user_id_users_id_fk", + "tableFrom": "homepage_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "homepage_items_sync_id_unique": { + "name": "homepage_items_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.homepage_layouts": { + "name": "homepage_layouts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_layouts_user_id_users_id_fk": { + "name": "homepage_layouts_user_id_users_id_fk", + "tableFrom": "homepage_layouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "homepage_layouts_user_id_unique": { + "name": "homepage_layouts_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_access": { + "name": "host_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_count": { + "name": "access_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "idx_host_access_user_id": { + "name": "idx_host_access_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_access_role_id": { + "name": "idx_host_access_role_id", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_access_host_id": { + "name": "idx_host_access_host_id", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_access_expires_at": { + "name": "idx_host_access_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_health_checks": { + "name": "host_health_checks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "checks": { + "name": "checks", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 300 + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_health_checks_user_host": { + "name": "idx_host_health_checks_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_health_checks_user_id_users_id_fk": { + "name": "host_health_checks_user_id_users_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_checks_host_id_ssh_data_id_fk": { + "name": "host_health_checks_host_id_ssh_data_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_health_history": { + "name": "host_health_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "ok": { + "name": "ok", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_health_history_user_id_users_id_fk": { + "name": "host_health_history_user_id_users_id_fk", + "tableFrom": "host_health_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_history_host_id_ssh_data_id_fk": { + "name": "host_health_history_host_id_ssh_data_id_fk", + "tableFrom": "host_health_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_metrics_history": { + "name": "host_metrics_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_metrics_history_host_id_ssh_data_id_fk": { + "name": "host_metrics_history_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_metrics_preferences": { + "name": "host_metrics_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_metrics_prefs_user_host": { + "name": "idx_host_metrics_prefs_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_metrics_preferences_user_id_users_id_fk": { + "name": "host_metrics_preferences_user_id_users_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_metrics_preferences_host_id_ssh_data_id_fk": { + "name": "host_metrics_preferences_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_data": { + "name": "ssh_data", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "folder": { + "name": "folder", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "parent_host_id": { + "name": "parent_host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_docker": { + "name": "enable_docker", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_web_ui": { + "name": "enable_web_ui", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_terminal_toolbar": { + "name": "enable_terminal_toolbar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_ai_assistant": { + "name": "enable_ai_assistant", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stats_config": { + "name": "stats_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "docker_config": { + "name": "docker_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "web_ui_config": { + "name": "web_ui_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_rdp": { + "name": "enable_rdp", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_vnc": { + "name": "enable_vnc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_telnet": { + "name": "enable_telnet", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "ssh_port": { + "name": "ssh_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 22 + }, + "rdp_port": { + "name": "rdp_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3389 + }, + "vnc_port": { + "name": "vnc_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 5900 + }, + "telnet_port": { + "name": "telnet_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 23 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_security": { + "name": "rdp_security", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_ignore_cert": { + "name": "rdp_ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "security": { + "name": "security", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ignore_cert": { + "name": "ignore_cert", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "guacamole_config": { + "name": "guacamole_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "wol_broadcast_address": { + "name": "wol_broadcast_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'sha256'" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_data_user_id": { + "name": "idx_ssh_data_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ssh_data_parent_host": { + "name": "idx_ssh_data_parent_host", + "columns": [ + { + "expression": "parent_host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ssh_data_credential": { + "name": "idx_ssh_data_credential", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_parent_host_id_ssh_data_id_fk": { + "name": "ssh_data_parent_host_id_ssh_data_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_data", + "columnsFrom": [ + "parent_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_channels": { + "name": "notification_channels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.opkssh_tokens": { + "name": "opkssh_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_install_counts": { + "name": "plugin_install_counts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "registry_id": { + "name": "registry_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'aggregate-telemetry'" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_install_counts_plugin_registry": { + "name": "idx_plugin_install_counts_plugin_registry", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "registry_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_migrations": { + "name": "plugin_migrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "migration_id": { + "name": "migration_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_migrations_plugin_migration": { + "name": "idx_plugin_migrations_plugin_migration", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "migration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_migrations_plugin_id_plugins_id_fk": { + "name": "plugin_migrations_plugin_id_plugins_id_fk", + "tableFrom": "plugin_migrations", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_permission_grants": { + "name": "plugin_permission_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "capability": { + "name": "capability", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'admin'" + } + }, + "indexes": { + "idx_plugin_permission_grants_plugin_capability": { + "name": "idx_plugin_permission_grants_plugin_capability", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "capability", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_permission_grants_plugin_id_plugins_id_fk": { + "name": "plugin_permission_grants_plugin_id_plugins_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_permission_grants_granted_by_users_id_fk": { + "name": "plugin_permission_grants_granted_by_users_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_registries": { + "name": "plugin_registries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'community'" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "signing_key": { + "name": "signing_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_checked_at": { + "name": "last_checked_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_index_hash": { + "name": "last_index_hash", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_settings": { + "name": "plugin_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted": { + "name": "encrypted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_settings_scope_key": { + "name": "idx_plugin_settings_scope_key", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_plugin_settings_plugin_scope": { + "name": "idx_plugin_settings_plugin_scope", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_settings_plugin_id_plugins_id_fk": { + "name": "plugin_settings_plugin_id_plugins_id_fk", + "tableFrom": "plugin_settings", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_storage": { + "name": "plugin_storage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_storage_plugin_key": { + "name": "idx_plugin_storage_plugin_key", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_storage_plugin_id_plugins_id_fk": { + "name": "plugin_storage_plugin_id_plugins_id_fk", + "tableFrom": "plugin_storage", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugins": { + "name": "plugins", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'available'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'community'" + }, + "registry_id": { + "name": "registry_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'disabled'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "installed_at": { + "name": "installed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "auto_update": { + "name": "auto_update", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "manifest_json": { + "name": "manifest_json", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_plugins_registry_id": { + "name": "idx_plugins_registry_id", + "columns": [ + { + "expression": "registry_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rbac_applied_defaults": { + "name": "rbac_applied_defaults", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "role_name": { + "name": "role_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_rbac_applied_defaults_role_permission": { + "name": "idx_rbac_applied_defaults_role_permission", + "columns": [ + { + "expression": "role_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rbac_known_permissions": { + "name": "rbac_known_permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_rbac_known_permissions_permission": { + "name": "idx_rbac_known_permissions_permission", + "columns": [ + { + "expression": "permission", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recent_activity": { + "name": "recent_activity", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_recent_activity_user_ts": { + "name": "idx_recent_activity_user_ts", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.roles": { + "name": "roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.secret_sources": { + "name": "secret_sources", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'onepassword-connect'" + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "shared": { + "name": "shared", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_secret_sources_user": { + "name": "idx_secret_sources_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "secret_sources_user_id_users_id_fk": { + "name": "secret_sources_user_id_users_id_fk", + "tableFrom": "secret_sources", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_recordings": { + "name": "session_recordings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_id": { + "name": "access_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "ended_at": { + "name": "ended_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "commands": { + "name": "commands", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dangerous_actions": { + "name": "dangerous_actions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "recording_path": { + "name": "recording_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "terminated_by_owner": { + "name": "terminated_by_owner", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "termination_reason": { + "name": "termination_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_session_recordings_user_started": { + "name": "idx_session_recordings_user_started", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_session_recordings_host": { + "name": "idx_session_recordings_host", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_recordings_host_id_ssh_data_id_fk": { + "name": "session_recordings_host_id_ssh_data_id_fk", + "tableFrom": "session_recordings", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_recordings_user_id_users_id_fk": { + "name": "session_recordings_user_id_users_id_fk", + "tableFrom": "session_recordings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "session_recordings_access_id_host_access_id_fk": { + "name": "session_recordings_access_id_host_access_id_fk", + "tableFrom": "session_recordings", + "tableTo": "host_access", + "columnsFrom": [ + "access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_share_participants": { + "name": "session_share_participants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "share_id": { + "name": "share_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "guest_label": { + "name": "guest_label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "joined_at": { + "name": "joined_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "left_at": { + "name": "left_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_share_participants_share_id_session_shares_id_fk": { + "name": "session_share_participants_share_id_session_shares_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "session_shares", + "columnsFrom": [ + "share_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_share_participants_user_id_users_id_fk": { + "name": "session_share_participants_user_id_users_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_shares": { + "name": "session_shares", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "tab_instance_id": { + "name": "tab_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "share_type": { + "name": "share_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "link_token": { + "name": "link_token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'read-only'" + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "revoked_at": { + "name": "revoked_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_joined_at": { + "name": "last_joined_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "join_count": { + "name": "join_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "idx_session_shares_session_id": { + "name": "idx_session_shares_session_id", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_session_shares_host_id": { + "name": "idx_session_shares_host_id", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_shares_host_id_ssh_data_id_fk": { + "name": "session_shares_host_id_ssh_data_id_fk", + "tableFrom": "session_shares", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_owner_user_id_users_id_fk": { + "name": "session_shares_owner_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_target_user_id_users_id_fk": { + "name": "session_shares_target_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_shares_link_token_unique": { + "name": "session_shares_link_token_unique", + "nullsNotDistinct": false, + "columns": [ + "link_token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_sessions_user_id": { + "name": "idx_sessions_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_sessions_expires_at": { + "name": "idx_sessions_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_credential_secrets": { + "name": "shared_credential_secrets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "credential_access_id": { + "name": "credential_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'password'" + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_credential_secrets_scope": { + "name": "idx_shared_credential_secrets_scope", + "columns": [ + { + "expression": "credential_access_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_shared_credential_secrets_target": { + "name": "idx_shared_credential_secrets_target", + "columns": [ + { + "expression": "target_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_credential_secrets_target_user_id_users_id_fk": { + "name": "shared_credential_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_credential_secrets_credential_id_ssh_credentials_id_fk": { + "name": "shared_credential_secrets_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_cred_secrets_access_id_fk": { + "name": "shared_cred_secrets_access_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "credential_access", + "columnsFrom": [ + "credential_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_host_secrets": { + "name": "shared_host_secrets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'credential'" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + { + "expression": "host_access_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_credential_usage": { + "name": "ssh_credential_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_credential_usage_credential": { + "name": "idx_ssh_credential_usage_credential", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ssh_credential_usage_user": { + "name": "idx_ssh_credential_usage_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_credentials": { + "name": "ssh_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_count": { + "name": "usage_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_credentials_user_id": { + "name": "idx_ssh_credentials_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_folders": { + "name": "ssh_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_folders_user_id": { + "name": "idx_ssh_folders_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_providers": { + "name": "sso_providers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_tombstones": { + "name": "sync_tombstones", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identities": { + "name": "termix_identities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "nullsNotDistinct": false, + "columns": [ + "handle" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identity_ca": { + "name": "termix_identity_ca", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "validity_days": { + "name": "validity_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "nullsNotDistinct": false, + "columns": [ + "identity_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identity_keys": { + "name": "termix_identity_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tmux_session_tags": { + "name": "tmux_session_tags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "session_name": { + "name": "session_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "tmux_session_tags_user_id_users_id_fk": { + "name": "tmux_session_tags_user_id_users_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tmux_session_tags_host_id_ssh_data_id_fk": { + "name": "tmux_session_tags_host_id_ssh_data_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.trusted_devices": { + "name": "trusted_devices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_trusted_devices_user_id": { + "name": "idx_trusted_devices_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ui_preferences": { + "name": "ui_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ui_preferences_user_id_users_id_fk": { + "name": "ui_preferences_user_id_users_id_fk", + "tableFrom": "ui_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_external_identities": { + "name": "user_external_identities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_external_identities_provider_subject": { + "name": "idx_user_external_identities_provider_subject", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_user_external_identities_user": { + "name": "idx_user_external_identities_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_external_identities_user_id_users_id_fk": { + "name": "user_external_identities_user_id_users_id_fk", + "tableFrom": "user_external_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_open_tabs": { + "name": "user_open_tabs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_open_tabs_user_id": { + "name": "idx_user_open_tabs_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_preferences": { + "name": "user_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "show_pin_app_rail_button": { + "name": "show_pin_app_rail_button", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ai_assistant_enabled": { + "name": "ai_assistant_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "ai_read_only_commands": { + "name": "ai_read_only_commands", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terminal_defaults": { + "name": "terminal_defaults", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_defaults": { + "name": "rdp_defaults", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terminal_macros": { + "name": "terminal_macros", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_roles": { + "name": "user_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_user_roles_role_id": { + "name": "idx_user_roles_role_id", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_second_factors": { + "name": "user_second_factors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "factor_id": { + "name": "factor_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "enrolled_at": { + "name": "enrolled_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_second_factors_user_factor": { + "name": "idx_user_second_factors_user_factor", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "factor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_second_factors_user_id_users_id_fk": { + "name": "user_second_factors_user_id_users_id_fk", + "tableFrom": "user_second_factors", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'openid email profile'" + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "totp_enabled": { + "name": "totp_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "totp_backup_codes": { + "name": "totp_backup_codes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vault_profiles": { + "name": "vault_profiles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shared": { + "name": "shared", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vault_tokens": { + "name": "vault_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "profile_id": { + "name": "profile_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webauthn_credentials": { + "name": "webauthn_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_verification": { + "name": "user_verification", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'preferred'" + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "webauthn_credentials_user_id_users_id_fk": { + "name": "webauthn_credentials_user_id_users_id_fk", + "tableFrom": "webauthn_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/postgres/meta/_journal.json b/drizzle/postgres/meta/_journal.json index 68c38092a..17a041f64 100644 --- a/drizzle/postgres/meta/_journal.json +++ b/drizzle/postgres/meta/_journal.json @@ -274,6 +274,13 @@ "when": 1790201004950, "tag": "0038_cute_vermin", "breakpoints": true + }, + { + "idx": 39, + "version": "7", + "when": 1790208809860, + "tag": "0039_violet_firedrake", + "breakpoints": true } ] } \ No newline at end of file diff --git a/drizzle/sqlite/0035_striped_strong_guy.sql b/drizzle/sqlite/0035_striped_strong_guy.sql new file mode 100644 index 000000000..c6b92fab6 --- /dev/null +++ b/drizzle/sqlite/0035_striped_strong_guy.sql @@ -0,0 +1,5 @@ +-- command_history moved to the ssh-terminal plugin, whose first migration +-- renames it to p_ssh_terminal_command_history. Dropping it here would lose +-- every saved command, so this only stops drizzle tracking it. SELECT 1 +-- because MySQL rejects an empty query. +SELECT 1; diff --git a/drizzle/sqlite/meta/0035_snapshot.json b/drizzle/sqlite/meta/0035_snapshot.json new file mode 100644 index 000000000..db722f0da --- /dev/null +++ b/drizzle/sqlite/meta/0035_snapshot.json @@ -0,0 +1,8030 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "0268e229-e62d-4118-8f80-8e12f1929bdb", + "prevId": "de96f9d2-2ee6-4342-9e5a-c71ab3b3f752", + "tables": { + "ai_conversations": { + "name": "ai_conversations", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_conversations_user": { + "name": "idx_ai_conversations_user", + "columns": [ + "user_id", + "updated_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ai_conversations_user_id_users_id_fk": { + "name": "ai_conversations_user_id_users_id_fk", + "tableFrom": "ai_conversations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ai_messages": { + "name": "ai_messages", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "tool_calls": { + "name": "tool_calls", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_messages_conversation": { + "name": "idx_ai_messages_conversation", + "columns": [ + "conversation_id", + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ai_messages_conversation_id_ai_conversations_id_fk": { + "name": "ai_messages_conversation_id_ai_conversations_id_fk", + "tableFrom": "ai_messages", + "tableTo": "ai_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ai_proposals": { + "name": "ai_proposals", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "result_summary": { + "name": "result_summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_proposals_user": { + "name": "idx_ai_proposals_user", + "columns": [ + "user_id", + "status" + ], + "isUnique": false + }, + "idx_ai_proposals_conversation": { + "name": "idx_ai_proposals_conversation", + "columns": [ + "conversation_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ai_proposals_conversation_id_ai_conversations_id_fk": { + "name": "ai_proposals_conversation_id_ai_conversations_id_fk", + "tableFrom": "ai_proposals", + "tableTo": "ai_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ai_proposals_user_id_users_id_fk": { + "name": "ai_proposals_user_id_users_id_fk", + "tableFrom": "ai_proposals", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ai_providers": { + "name": "ai_providers", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_type": { + "name": "provider_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "api_key": { + "name": "api_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "api_key_prefix": { + "name": "api_key_prefix", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_model": { + "name": "default_model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_providers_user_label": { + "name": "idx_ai_providers_user_label", + "columns": [ + "user_id", + "label" + ], + "isUnique": true + } + }, + "foreignKeys": { + "ai_providers_user_id_users_id_fk": { + "name": "ai_providers_user_id_users_id_fk", + "tableFrom": "ai_providers", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "api_keys": { + "name": "api_keys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": { + "idx_api_keys_user_id": { + "name": "idx_api_keys_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "success": { + "name": "success", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_audit_logs_timestamp": { + "name": "idx_audit_logs_timestamp", + "columns": [ + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_user_ts": { + "name": "idx_audit_logs_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_action_ts": { + "name": "idx_audit_logs_action_ts", + "columns": [ + "action", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_resource_ts": { + "name": "idx_audit_logs_resource_ts", + "columns": [ + "resource_type", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "automation_channels": { + "name": "automation_channels", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "automation_id": { + "name": "automation_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel_id": { + "name": "channel_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_automation_channels_pair": { + "name": "idx_automation_channels_pair", + "columns": [ + "automation_id", + "channel_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "automation_channels_automation_id_automations_id_fk": { + "name": "automation_channels_automation_id_automations_id_fk", + "tableFrom": "automation_channels", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_channels_channel_id_notification_channels_id_fk": { + "name": "automation_channels_channel_id_notification_channels_id_fk", + "tableFrom": "automation_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "automation_run_steps": { + "name": "automation_run_steps", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "run_id": { + "name": "run_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "step_index": { + "name": "step_index", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "step_id": { + "name": "step_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "step_type": { + "name": "step_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "finished_at": { + "name": "finished_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "output": { + "name": "output", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "truncated": { + "name": "truncated", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "idx_automation_run_steps_run": { + "name": "idx_automation_run_steps_run", + "columns": [ + "run_id", + "step_index" + ], + "isUnique": false + } + }, + "foreignKeys": { + "automation_run_steps_run_id_automation_runs_id_fk": { + "name": "automation_run_steps_run_id_automation_runs_id_fk", + "tableFrom": "automation_run_steps", + "tableTo": "automation_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "automation_runs": { + "name": "automation_runs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "automation_id": { + "name": "automation_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "trigger_context": { + "name": "trigger_context", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "finished_at": { + "name": "finished_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dry_run": { + "name": "dry_run", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_automation_runs_automation": { + "name": "idx_automation_runs_automation", + "columns": [ + "automation_id", + "started_at" + ], + "isUnique": false + }, + "idx_automation_runs_user": { + "name": "idx_automation_runs_user", + "columns": [ + "user_id", + "started_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "automation_runs_automation_id_automations_id_fk": { + "name": "automation_runs_automation_id_automations_id_fk", + "tableFrom": "automation_runs", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_runs_user_id_users_id_fk": { + "name": "automation_runs_user_id_users_id_fk", + "tableFrom": "automation_runs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "automation_schedules": { + "name": "automation_schedules", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "automation_id": { + "name": "automation_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cron": { + "name": "cron", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "next_due_at": { + "name": "next_due_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_tick_at": { + "name": "last_tick_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_automation_schedules_automation": { + "name": "idx_automation_schedules_automation", + "columns": [ + "automation_id" + ], + "isUnique": true + }, + "idx_automation_schedules_due": { + "name": "idx_automation_schedules_due", + "columns": [ + "next_due_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "automation_schedules_automation_id_automations_id_fk": { + "name": "automation_schedules_automation_id_automations_id_fk", + "tableFrom": "automation_schedules", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "automation_trigger_state": { + "name": "automation_trigger_state", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "automation_id": { + "name": "automation_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "state_key": { + "name": "state_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "breach_started_at": { + "name": "breach_started_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_value": { + "name": "last_value", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_observed_state": { + "name": "last_observed_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_automation_trigger_state_key": { + "name": "idx_automation_trigger_state_key", + "columns": [ + "automation_id", + "state_key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "automation_trigger_state_automation_id_automations_id_fk": { + "name": "automation_trigger_state_automation_id_automations_id_fk", + "tableFrom": "automation_trigger_state", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "automations": { + "name": "automations", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "definition": { + "name": "definition", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "definition_version": { + "name": "definition_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "concurrency_policy": { + "name": "concurrency_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'skip'" + }, + "max_run_seconds": { + "name": "max_run_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 300 + }, + "dry_run": { + "name": "dry_run", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "last_run_at": { + "name": "last_run_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_run_status": { + "name": "last_run_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_automations_user": { + "name": "idx_automations_user", + "columns": [ + "user_id", + "enabled" + ], + "isUnique": false + } + }, + "foreignKeys": { + "automations_user_id_users_id_fk": { + "name": "automations_user_id_users_id_fk", + "tableFrom": "automations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "collab_room_members": { + "name": "collab_room_members", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "room_id": { + "name": "room_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "room_role": { + "name": "room_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'member'" + }, + "added_by": { + "name": "added_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_collab_room_members_room_user": { + "name": "idx_collab_room_members_room_user", + "columns": [ + "room_id", + "user_id" + ], + "isUnique": true + }, + "idx_collab_room_members_user": { + "name": "idx_collab_room_members_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "collab_room_members_room_id_collab_rooms_id_fk": { + "name": "collab_room_members_room_id_collab_rooms_id_fk", + "tableFrom": "collab_room_members", + "tableTo": "collab_rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "collab_room_members_user_id_users_id_fk": { + "name": "collab_room_members_user_id_users_id_fk", + "tableFrom": "collab_room_members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "collab_room_members_added_by_users_id_fk": { + "name": "collab_room_members_added_by_users_id_fk", + "tableFrom": "collab_room_members", + "tableTo": "users", + "columnsFrom": [ + "added_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "collab_rooms": { + "name": "collab_rooms", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "persistent": { + "name": "persistent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "presenter_user_id": { + "name": "presenter_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stage_protocol": { + "name": "stage_protocol", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stage_host_id": { + "name": "stage_host_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stage_share_id": { + "name": "stage_share_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "guest_link_token": { + "name": "guest_link_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "ended_at": { + "name": "ended_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_collab_rooms_owner": { + "name": "idx_collab_rooms_owner", + "columns": [ + "owner_user_id" + ], + "isUnique": false + }, + "idx_collab_rooms_guest_token": { + "name": "idx_collab_rooms_guest_token", + "columns": [ + "guest_link_token" + ], + "isUnique": true + } + }, + "foreignKeys": { + "collab_rooms_owner_user_id_users_id_fk": { + "name": "collab_rooms_owner_user_id_users_id_fk", + "tableFrom": "collab_rooms", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "collab_rooms_presenter_user_id_users_id_fk": { + "name": "collab_rooms_presenter_user_id_users_id_fk", + "tableFrom": "collab_rooms", + "tableTo": "users", + "columnsFrom": [ + "presenter_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "collab_rooms_stage_host_id_ssh_data_id_fk": { + "name": "collab_rooms_stage_host_id_ssh_data_id_fk", + "tableFrom": "collab_rooms", + "tableTo": "ssh_data", + "columnsFrom": [ + "stage_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "collab_rooms_stage_share_id_session_shares_id_fk": { + "name": "collab_rooms_stage_share_id_session_shares_id_fk", + "tableFrom": "collab_rooms", + "tableTo": "session_shares", + "columnsFrom": [ + "stage_share_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "credential_access": { + "name": "credential_access", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'use'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_credential_access_user_id": { + "name": "idx_credential_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_credential_access_role_id": { + "name": "idx_credential_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + }, + "idx_credential_access_credential_id": { + "name": "idx_credential_access_credential_id", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "credential_access_credential_id_ssh_credentials_id_fk": { + "name": "credential_access_credential_id_ssh_credentials_id_fk", + "tableFrom": "credential_access", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_user_id_users_id_fk": { + "name": "credential_access_user_id_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_role_id_roles_id_fk": { + "name": "credential_access_role_id_roles_id_fk", + "tableFrom": "credential_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_granted_by_users_id_fk": { + "name": "credential_access_granted_by_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "dashboard_service_links": { + "name": "dashboard_service_links", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "dashboard_service_links_sync_id_unique": { + "name": "dashboard_service_links_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "dashboard_service_links_user_id_users_id_fk": { + "name": "dashboard_service_links_user_id_users_id_fk", + "tableFrom": "dashboard_service_links", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "dismissed_alerts": { + "name": "dismissed_alerts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_dismissed_alerts_user_id": { + "name": "idx_dismissed_alerts_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "folder_access": { + "name": "folder_access", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_folder_access_owner_folder": { + "name": "idx_folder_access_owner_folder", + "columns": [ + "owner_user_id", + "folder" + ], + "isUnique": false + } + }, + "foreignKeys": { + "folder_access_owner_user_id_users_id_fk": { + "name": "folder_access_owner_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_user_id_users_id_fk": { + "name": "folder_access_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_role_id_roles_id_fk": { + "name": "folder_access_role_id_roles_id_fk", + "tableFrom": "folder_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_granted_by_users_id_fk": { + "name": "folder_access_granted_by_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "homepage_items": { + "name": "homepage_items", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "folder_id": { + "name": "folder_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "homepage_items_sync_id_unique": { + "name": "homepage_items_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_homepage_items_user_id": { + "name": "idx_homepage_items_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "homepage_items_user_id_users_id_fk": { + "name": "homepage_items_user_id_users_id_fk", + "tableFrom": "homepage_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "homepage_layouts": { + "name": "homepage_layouts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "homepage_layouts_user_id_unique": { + "name": "homepage_layouts_user_id_unique", + "columns": [ + "user_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "homepage_layouts_user_id_users_id_fk": { + "name": "homepage_layouts_user_id_users_id_fk", + "tableFrom": "homepage_layouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_access": { + "name": "host_access", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_count": { + "name": "access_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "idx_host_access_user_id": { + "name": "idx_host_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_host_access_role_id": { + "name": "idx_host_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + }, + "idx_host_access_host_id": { + "name": "idx_host_access_host_id", + "columns": [ + "host_id" + ], + "isUnique": false + }, + "idx_host_access_expires_at": { + "name": "idx_host_access_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_health_checks": { + "name": "host_health_checks", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checks": { + "name": "checks", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 300 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_health_checks_user_host": { + "name": "idx_host_health_checks_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_health_checks_user_id_users_id_fk": { + "name": "host_health_checks_user_id_users_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_checks_host_id_ssh_data_id_fk": { + "name": "host_health_checks_host_id_ssh_data_id_fk", + "tableFrom": "host_health_checks", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_health_history": { + "name": "host_health_history", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "ok": { + "name": "ok", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_health_history_user_id_users_id_fk": { + "name": "host_health_history_user_id_users_id_fk", + "tableFrom": "host_health_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_health_history_host_id_ssh_data_id_fk": { + "name": "host_health_history_host_id_ssh_data_id_fk", + "tableFrom": "host_health_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_metrics_history": { + "name": "host_metrics_history", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mem_percent": { + "name": "mem_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_percent": { + "name": "disk_percent", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_rx_bytes": { + "name": "net_rx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "net_tx_bytes": { + "name": "net_tx_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "host_metrics_history_host_id_ssh_data_id_fk": { + "name": "host_metrics_history_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_history", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_metrics_preferences": { + "name": "host_metrics_preferences", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_metrics_prefs_user_host": { + "name": "idx_host_metrics_prefs_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "host_metrics_preferences_user_id_users_id_fk": { + "name": "host_metrics_preferences_user_id_users_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_metrics_preferences_host_id_ssh_data_id_fk": { + "name": "host_metrics_preferences_host_id_ssh_data_id_fk", + "tableFrom": "host_metrics_preferences", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_data": { + "name": "ssh_data", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_host_id": { + "name": "parent_host_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_docker": { + "name": "enable_docker", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_web_ui": { + "name": "enable_web_ui", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_terminal_toolbar": { + "name": "enable_terminal_toolbar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_ai_assistant": { + "name": "enable_ai_assistant", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stats_config": { + "name": "stats_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "docker_config": { + "name": "docker_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "web_ui_config": { + "name": "web_ui_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_rdp": { + "name": "enable_rdp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_vnc": { + "name": "enable_vnc", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_telnet": { + "name": "enable_telnet", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "ssh_port": { + "name": "ssh_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "rdp_port": { + "name": "rdp_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 3389 + }, + "vnc_port": { + "name": "vnc_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 5900 + }, + "telnet_port": { + "name": "telnet_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 23 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_security": { + "name": "rdp_security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_ignore_cert": { + "name": "rdp_ignore_cert", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "security": { + "name": "security", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ignore_cert": { + "name": "ignore_cert", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "guacamole_config": { + "name": "guacamole_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "wol_broadcast_address": { + "name": "wol_broadcast_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'sha256'" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_ssh_data_user_id": { + "name": "idx_ssh_data_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_ssh_data_parent_host": { + "name": "idx_ssh_data_parent_host", + "columns": [ + "parent_host_id" + ], + "isUnique": false + }, + "idx_ssh_data_credential": { + "name": "idx_ssh_data_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_parent_host_id_ssh_data_id_fk": { + "name": "ssh_data_parent_host_id_ssh_data_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_data", + "columnsFrom": [ + "parent_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_channels": { + "name": "notification_channels", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "opkssh_tokens": { + "name": "opkssh_tokens", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_install_counts": { + "name": "plugin_install_counts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "registry_id": { + "name": "registry_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'aggregate-telemetry'" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_install_counts_plugin_registry": { + "name": "idx_plugin_install_counts_plugin_registry", + "columns": [ + "plugin_id", + "registry_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_migrations": { + "name": "plugin_migrations", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "migration_id": { + "name": "migration_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_migrations_plugin_migration": { + "name": "idx_plugin_migrations_plugin_migration", + "columns": [ + "plugin_id", + "migration_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_migrations_plugin_id_plugins_id_fk": { + "name": "plugin_migrations_plugin_id_plugins_id_fk", + "tableFrom": "plugin_migrations", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_permission_grants": { + "name": "plugin_permission_grants", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "capability": { + "name": "capability", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'admin'" + } + }, + "indexes": { + "idx_plugin_permission_grants_plugin_capability": { + "name": "idx_plugin_permission_grants_plugin_capability", + "columns": [ + "plugin_id", + "capability" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_permission_grants_plugin_id_plugins_id_fk": { + "name": "plugin_permission_grants_plugin_id_plugins_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_permission_grants_granted_by_users_id_fk": { + "name": "plugin_permission_grants_granted_by_users_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_registries": { + "name": "plugin_registries", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'community'" + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "signing_key": { + "name": "signing_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_checked_at": { + "name": "last_checked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_index_hash": { + "name": "last_index_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_settings": { + "name": "plugin_settings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted": { + "name": "encrypted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_settings_scope_key": { + "name": "idx_plugin_settings_scope_key", + "columns": [ + "plugin_id", + "scope", + "scope_id", + "key" + ], + "isUnique": true + }, + "idx_plugin_settings_plugin_scope": { + "name": "idx_plugin_settings_plugin_scope", + "columns": [ + "plugin_id", + "scope" + ], + "isUnique": false + } + }, + "foreignKeys": { + "plugin_settings_plugin_id_plugins_id_fk": { + "name": "plugin_settings_plugin_id_plugins_id_fk", + "tableFrom": "plugin_settings", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_storage": { + "name": "plugin_storage", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_storage_plugin_key": { + "name": "idx_plugin_storage_plugin_key", + "columns": [ + "plugin_id", + "key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_storage_plugin_id_plugins_id_fk": { + "name": "plugin_storage_plugin_id_plugins_id_fk", + "tableFrom": "plugin_storage", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugins": { + "name": "plugins", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'available'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'community'" + }, + "registry_id": { + "name": "registry_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'disabled'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "installed_at": { + "name": "installed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "auto_update": { + "name": "auto_update", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "manifest_json": { + "name": "manifest_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_plugins_registry_id": { + "name": "idx_plugins_registry_id", + "columns": [ + "registry_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rbac_applied_defaults": { + "name": "rbac_applied_defaults", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "role_name": { + "name": "role_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_rbac_applied_defaults_role_permission": { + "name": "idx_rbac_applied_defaults_role_permission", + "columns": [ + "role_name", + "permission" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rbac_known_permissions": { + "name": "rbac_known_permissions", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_rbac_known_permissions_permission": { + "name": "idx_rbac_known_permissions_permission", + "columns": [ + "permission" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "recent_activity": { + "name": "recent_activity", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_recent_activity_user_ts": { + "name": "idx_recent_activity_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "secret_sources": { + "name": "secret_sources", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'onepassword-connect'" + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "shared": { + "name": "shared", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_secret_sources_user": { + "name": "idx_secret_sources_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "secret_sources_user_id_users_id_fk": { + "name": "secret_sources_user_id_users_id_fk", + "tableFrom": "secret_sources", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_recordings": { + "name": "session_recordings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_id": { + "name": "access_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "ended_at": { + "name": "ended_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "commands": { + "name": "commands", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dangerous_actions": { + "name": "dangerous_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recording_path": { + "name": "recording_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'text'" + }, + "terminated_by_owner": { + "name": "terminated_by_owner", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "termination_reason": { + "name": "termination_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_session_recordings_user_started": { + "name": "idx_session_recordings_user_started", + "columns": [ + "user_id", + "started_at" + ], + "isUnique": false + }, + "idx_session_recordings_host": { + "name": "idx_session_recordings_host", + "columns": [ + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "session_recordings_host_id_ssh_data_id_fk": { + "name": "session_recordings_host_id_ssh_data_id_fk", + "tableFrom": "session_recordings", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_recordings_user_id_users_id_fk": { + "name": "session_recordings_user_id_users_id_fk", + "tableFrom": "session_recordings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "session_recordings_access_id_host_access_id_fk": { + "name": "session_recordings_access_id_host_access_id_fk", + "tableFrom": "session_recordings", + "tableTo": "host_access", + "columnsFrom": [ + "access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_share_participants": { + "name": "session_share_participants", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "share_id": { + "name": "share_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "guest_label": { + "name": "guest_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "joined_at": { + "name": "joined_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "left_at": { + "name": "left_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_share_participants_share_id_session_shares_id_fk": { + "name": "session_share_participants_share_id_session_shares_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "session_shares", + "columnsFrom": [ + "share_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_share_participants_user_id_users_id_fk": { + "name": "session_share_participants_user_id_users_id_fk", + "tableFrom": "session_share_participants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_shares": { + "name": "session_shares", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_instance_id": { + "name": "tab_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "share_type": { + "name": "share_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "link_token": { + "name": "link_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'read-only'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_joined_at": { + "name": "last_joined_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "join_count": { + "name": "join_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "session_shares_link_token_unique": { + "name": "session_shares_link_token_unique", + "columns": [ + "link_token" + ], + "isUnique": true + }, + "idx_session_shares_session_id": { + "name": "idx_session_shares_session_id", + "columns": [ + "session_id" + ], + "isUnique": false + }, + "idx_session_shares_host_id": { + "name": "idx_session_shares_host_id", + "columns": [ + "host_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "session_shares_host_id_ssh_data_id_fk": { + "name": "session_shares_host_id_ssh_data_id_fk", + "tableFrom": "session_shares", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_owner_user_id_users_id_fk": { + "name": "session_shares_owner_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_shares_target_user_id_users_id_fk": { + "name": "session_shares_target_user_id_users_id_fk", + "tableFrom": "session_shares", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_sessions_user_id": { + "name": "idx_sessions_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_sessions_expires_at": { + "name": "idx_sessions_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shared_credential_secrets": { + "name": "shared_credential_secrets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "credential_access_id": { + "name": "credential_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'password'" + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(4096)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_credential_secrets_scope": { + "name": "idx_shared_credential_secrets_scope", + "columns": [ + "credential_access_id", + "target_user_id" + ], + "isUnique": true + }, + "idx_shared_credential_secrets_target": { + "name": "idx_shared_credential_secrets_target", + "columns": [ + "target_user_id", + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "shared_credential_secrets_target_user_id_users_id_fk": { + "name": "shared_credential_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_credential_secrets_credential_id_ssh_credentials_id_fk": { + "name": "shared_credential_secrets_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_cred_secrets_access_id_fk": { + "name": "shared_cred_secrets_access_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "credential_access", + "columnsFrom": [ + "credential_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + "host_id", + "user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shared_host_secrets": { + "name": "shared_host_secrets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'credential'" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + "host_access_id", + "target_user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_credential_usage": { + "name": "ssh_credential_usage", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_credential_usage_credential": { + "name": "idx_ssh_credential_usage_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + }, + "idx_ssh_credential_usage_user": { + "name": "idx_ssh_credential_usage_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_credentials": { + "name": "ssh_credentials", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(4096)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "usage_count": { + "name": "usage_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_ssh_credentials_user_id": { + "name": "idx_ssh_credentials_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_folders": { + "name": "ssh_folders", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_ssh_folders_user_id": { + "name": "idx_ssh_folders_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sso_providers": { + "name": "sso_providers", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sync_tombstones": { + "name": "sync_tombstones", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "termix_identities": { + "name": "termix_identities", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "columns": [ + "user_id" + ], + "isUnique": true + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "columns": [ + "handle" + ], + "isUnique": true + } + }, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "termix_identity_ca": { + "name": "termix_identity_ca", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(4096)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "validity_days": { + "name": "validity_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "columns": [ + "identity_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "termix_identity_keys": { + "name": "termix_identity_keys", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'manual'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tmux_session_tags": { + "name": "tmux_session_tags", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_name": { + "name": "session_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "tmux_session_tags_user_id_users_id_fk": { + "name": "tmux_session_tags_user_id_users_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tmux_session_tags_host_id_ssh_data_id_fk": { + "name": "tmux_session_tags_host_id_ssh_data_id_fk", + "tableFrom": "tmux_session_tags", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "trusted_devices": { + "name": "trusted_devices", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_trusted_devices_user_id": { + "name": "idx_trusted_devices_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ui_preferences": { + "name": "ui_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ui_preferences_user_id_users_id_fk": { + "name": "ui_preferences_user_id_users_id_fk", + "tableFrom": "ui_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_external_identities": { + "name": "user_external_identities", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_external_identities_provider_subject": { + "name": "idx_user_external_identities_provider_subject", + "columns": [ + "provider_id", + "subject" + ], + "isUnique": true + }, + "idx_user_external_identities_user": { + "name": "idx_user_external_identities_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_external_identities_user_id_users_id_fk": { + "name": "user_external_identities_user_id_users_id_fk", + "tableFrom": "user_external_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_open_tabs": { + "name": "user_open_tabs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_open_tabs_user_id": { + "name": "idx_user_open_tabs_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_pin_app_rail_button": { + "name": "show_pin_app_rail_button", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ai_assistant_enabled": { + "name": "ai_assistant_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ai_read_only_commands": { + "name": "ai_read_only_commands", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_defaults": { + "name": "terminal_defaults", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_defaults": { + "name": "rdp_defaults", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_macros": { + "name": "terminal_macros", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + "user_id", + "role_id" + ], + "isUnique": true + }, + "idx_user_roles_role_id": { + "name": "idx_user_roles_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_second_factors": { + "name": "user_second_factors", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "factor_id": { + "name": "factor_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enrolled_at": { + "name": "enrolled_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_second_factors_user_factor": { + "name": "idx_user_second_factors_user_factor", + "columns": [ + "user_id", + "plugin_id", + "factor_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_second_factors_user_id_users_id_fk": { + "name": "user_second_factors_user_id_users_id_fk", + "tableFrom": "user_second_factors", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_admin": { + "name": "is_admin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'openid email profile'" + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totp_enabled": { + "name": "totp_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "totp_backup_codes": { + "name": "totp_backup_codes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vault_profiles": { + "name": "vault_profiles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shared": { + "name": "shared", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vault_tokens": { + "name": "vault_tokens", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "profile_id": { + "name": "profile_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + "user_id", + "profile_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "webauthn_credentials": { + "name": "webauthn_credentials", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "backed_up": { + "name": "backed_up", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_verification": { + "name": "user_verification", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'preferred'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "webauthn_credentials_user_id_users_id_fk": { + "name": "webauthn_credentials_user_id_users_id_fk", + "tableFrom": "webauthn_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/sqlite/meta/_journal.json b/drizzle/sqlite/meta/_journal.json index 2cd3071c9..ea869bade 100644 --- a/drizzle/sqlite/meta/_journal.json +++ b/drizzle/sqlite/meta/_journal.json @@ -246,6 +246,13 @@ "when": 1790201004314, "tag": "0034_curious_molecule_man", "breakpoints": true + }, + { + "idx": 35, + "version": "6", + "when": 1790208809221, + "tag": "0035_striped_strong_guy", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/plugin-sdk/ARCHITECTURE.md b/packages/plugin-sdk/ARCHITECTURE.md index 0f2adce82..e872a608d 100644 --- a/packages/plugin-sdk/ARCHITECTURE.md +++ b/packages/plugin-sdk/ARCHITECTURE.md @@ -248,6 +248,20 @@ arrives with a share token), the Docker console (per-message auth), and the Guacamole display (a single-use encrypted connection token in the query, minted by an authenticated route). +**B9** added `optionalAuth` for a public route: core still verifies a token +when the client sent one and hands the handler that user, else an empty id. +The terminal uses it, because one socket serves both signed-in users and +share-link or room guests, who arrive with a share token instead. The route is +public so a guest can reach it at all; guest auth itself stays in the plugin's +handler (noted for D2). Every route connection now also carries `clientIp`, +`requestOrigin` and `isDataUnlocked()`, the data-key check a long-lived socket +repeats per message so an expired session stops being served. + +Socket and ssh2 event listeners fire outside the upgrade's async context, so a +plugin that calls privileged ctx members from them binds them with Node's +`AsyncResource.bind` while the actor is still set; the terminal does this for +every listener it registers. + ### 7. Settings Plugins declare settings fields in the manifest with scope `admin`, `user` or @@ -426,8 +440,10 @@ to list manager cards plugins added to `host-metrics.managers` next to its own). The SDK has no runtime dependencies: the hooks delegate to a host bridge core installs. -Slots core owns: `terminal.toolbar`, `terminal.dock`, `terminal.overlay` -(declared by ssh-terminal), `onboarding.steps`, `onboarding.features`, +Slots core owns: `terminal.toolbar`, `terminal.toolbarStatus`, +`terminal.sidePanel`, `terminal.overlay` (declared by ssh-terminal; **B9** +renamed `terminal.dock` to `terminal.sidePanel` and added the status slot, +where host-metrics puts its CPU, memory and disk bars), `onboarding.steps`, `onboarding.features`, `onboarding.workflow`, `hosts.importMenu`, `hosts.panel`, `proxmox.hostEditor` and `session.remoteDisplay`. `host-metrics.managers` (component slot, **B3**) is host-metrics's own: tailscale contributes its manager card there @@ -438,6 +454,29 @@ plugin actions the same way when a core view shows a plugin's data: `files.openHost`, and **B7**'s `tunnels.statuses` and `tunnels.open` behind the dashboard's active tunnel counter, which reads as zero while tunnels is off. +**B9** moved the terminal itself into ssh-terminal, which adds +`terminal.open(host, { path?, joinSharedSessionId?, joinShareId?, label? })` +(the file manager's "open terminal here", the connections panel's "join a +shared session") and replaces `ShellApi.openTerminalTab`, which the shell no +longer has. + +**Components by id.** `app.registerComponent(id, component)` offers a +component to other plugins and core, which render it with `PluginComponent` +from `@termix/plugin-sdk/ui` (a fallback while the owner is off) or +`usePluginComponent(id)`. ssh-terminal registers `terminal.view`, the +embeddable terminal the collab room, the homepage SSH widget, the tmux +monitor and the file manager's window render instead of importing it; its +handle comes back through a `handleRef` prop, since a registered component +cannot take a React ref. + +**Session tabs.** `TabOptions` gained `commandTarget` (history, macros and SSH +tools act on the last one focused; `PanelProps.targetTab` hands a panel that +tab), `ownBackground` (the shell leaves the frame transparent) and +`multiInstance` (each open is a new tab). `TabHandle` documents what a session +tab puts on `handleRef`: focus, reconnect, disconnect and, for sharing, +`getShareTarget()`, which the shell's own share dialog uses. These replaced +every check of the `terminal` and `local-terminal` tab types in the shell. + **B2** added a live-terminal-session surface, for a plugin that needs to push resolved text into an open SSH session rather than just fill a slot with a button: ssh-terminal registers `terminal.listSessions()` (open sessions with @@ -466,12 +505,28 @@ skipped, and it stays in the next snapshot (`isUnregisteredPluginTabType` in `src/ui/plugin-host/sdk-ui.ts`. Adding to it is a contract change; removing from it is a breaking one. Today it exports: alert, alert-dialog, badge, button, card, checkbox, dialog, dropdown-menu, input, label, password-input, -select, select2, separator, switch, textarea, tooltip, section-card, +select, select2, separator, switch, textarea, tooltip, sheet, section-card, metric-card, charts, the card grid, `ConnectionScreen` and the connection status helpers, `SnippetVariablesDialog`, `FullScreenAppWrapper`, the connection log context, `TOTPDialog`, `SSHAuthDialog`, `WarpgateDialog`, -`useTabs`/`useTabsSafe`, `ActionSlot`, `ComponentSlot` and `FOLDER_COLORS` (the -colour swatches folders and workspaces pick from). Publishing its +`PassphraseDialog`, `OPKSSHDialog` (until opkssh moves in Phase C), +`HostKeyVerificationDialog`, `useTabs`/`useTabsSafe`, `ActionSlot`, +`ComponentSlot`, `PluginComponent` and `FOLDER_COLORS` (the colour swatches +folders and workspaces pick from). + +**B9** added what a terminal-like surface needs from the shell: `cn`, +`useConfirmation`, `useIsMobile`, `runAdaptivePolling`; the terminal look +(`TERMINAL_THEMES`, `TERMINAL_FONTS`, `DEFAULT_TERMINAL_CONFIG`, +`resolveTermixThemeColors`, `ensureTerminalFontsLoaded`, the font zoom limits, +the clipboard helpers and `useAppTheme`, the full theme id rather than light or +dark), shared by the SSH and local terminals, the docker console and serial +from `src/ui/lib/terminal-look/`; keyboard handling shared with the shell +(`findMatchingKeybinding`, `globalShortcutHandler`, `isTabJumpHotkey`); the +connection helpers (`isElectron`, `resolveConnectionOrigin`, `pluginWsUrl`, +`hydrateLocalSharedHostAuth`, `useConnectionDefaults`); and a few core APIs +(`logActivity`, `getHostPassword`, `patchOpenTab`, `getUserPreferences`, +`parseCustomKeybindings`, `setHostAutoTmux`, `getCookie`). The APIs are a +stopgap in the right place: D1 turns them into typed bridge members. Publishing its `.d.ts` for plugins outside this repo is a follow-up for the repo split. #### Strings @@ -520,11 +575,34 @@ every enabled plugin that declares `contributes.settings.host`, it looks up `".hostImportNormalizer"` and, if the plugin registered one, calls it with the raw imported row and writes back whatever it returns (or nothing, for `null`). This is not part of the SDK's typed `ctx` surface - it is a plain -`ctx.registry` convention, the same mechanism `terminal.sessions` and -`remote-desktop.sessions` already use, just with a name core's own code knows +`ctx.registry` convention, the same mechanism `remote-desktop.sessions` +already uses, just with a name core's own code knows to look for. web-endpoint (`src/backend/host-import.ts`) is the first plugin to register one. +**B9**'s terminal provides `sessions.live` (live SSH sessions: look up, end, +remove a share's participants, hand a room share's control over, subscribe to +output, write) and `terminal.history` (the actor's command history, which the +AI assistant reads). It consumes three optional services that later steps +provide: `tmux.sessions` (B10; without it the terminal skips tmux attach), +`sessions.sharing` (B12; member joins and collab room events) and +`recordings.writer` (B13; without it nothing is recorded). The terminal keeps +the asciicast formatting and the 300 ms batched flush (issue #1049) and hands +the provider one `append` per batch. Service names need a dot, so "tmux" and +"recordings" became `tmux.sessions` and `recordings.writer`. + +A service call runs a permission check for its actor, so a share-link guest, +who has no user, cannot make one. Guest link resolution is therefore +published on `ctx.registry` as `sessions.sharing.guests` instead: the token is +the authority there. + +**Core consuming a plugin service.** Session sharing, collab and the open +tabs route are still core and reach live terminal sessions through +`getServiceImplementation(service, range)` in `service-registry.ts` +(`hosts/live-terminal-sessions.ts` wraps it). Core already authorized the +request, so this skips the per-call check and returns the same object +plugins get, or undefined while no compatible provider runs. + ### 10. Lifecycle `activate(ctx)` creates all state. Everything registered through `ctx` or `app` @@ -1082,6 +1160,7 @@ Built per plugin in `src/backend/plugins/ctx.ts` and passed to `activate`. | `ctx.notify.*` | `notify:send` | A6 | | `ctx.auth.*` | `auth:provide` | **A8** | | `ctx.desktop.openIsolatedWindow` | `desktop:window` | **B8** | +| `ctx.audit.record` | none, the actor is the runtime's | **B9** | | `ctx.fetch` | `network:outbound` | B | `ctx.ssh` was pulled forward from B so plugin transports go through core's @@ -1107,6 +1186,15 @@ connect pipeline instead of importing ssh2 helpers from core: since the file-manager plugin's interactive connect route and its dedicated transfer sessions both need their own keepalive/timeout defaults rather than falling back to the generic `"plugin"` purpose. +- **B9** added what an interactive transport needs, for the terminal: + `resolveHost(hostId, { syncId? })` (the host with secrets, by sync id first, + audited), the `"terminal"` purpose, `interactive` and `hostKeySocket` on + `prepare` (so a changed host key can be accepted over the socket), the + provider's `onBanner` and `onAuthFailed` hooks bound on its result, + `openTransport(..., { resolveDns, log })`, and `startInteraction` / + `cancelInteraction` for the browser sign-ins (opkssh, step-ca, vault), which + the terminal used to reach by import. Providers gained an optional + `cancelInteraction`. - **B7** added `sock` to the connect options: an already-open stream to the host, such as a `forwardOut` channel through another host. `connect` then skips the transport step (port knocking, proxy, jump hosts, DNS) and runs @@ -1165,6 +1253,12 @@ existing ones (proxmox's discovery and sync flow is the first caller): detail" surface those calls need to read back, not a wider read grant than `hosts:read` gives through `list`/`get`. +**B9** added `trackSession(hostId)` (the online indicator, counted with every +other feature's sessions; synchronous, so only the declaration is checked, like +`ctx.http.router`) and `recordActivity(hostId, type, hostName)` (the same rate +limit and access rule as the dashboard's route, which now shares +`services/recent-activity.ts` with it). Both need `hosts:read`. + `PluginHostRecord` carries no secret auth material (password, key, vault token): a plugin that creates a host picks an `authType` and, for `"credential"`, a `credentialId` it does not need to see the contents of. @@ -1318,7 +1412,8 @@ and that is bundled into its output. **Backend** (plus every node builtin): `@termix/plugin-sdk`, `express`, `ssh2`, `ws`, `multer`, `cookie-parser`, `axios`, `jszip`, `guacamole-lite`, -`@anthropic-ai/sdk`, `drizzle-orm`. +`@anthropic-ai/sdk`, `drizzle-orm`, `sharp` (native, so it cannot be bundled; +the terminal's image upload loads it on first use). **Frontend**: `react`, `react-dom`, `react-dom/client`, `react/jsx-runtime`, `i18next`, `react-i18next`, `sonner`, `@termix/plugin-sdk/frontend`, @@ -1666,8 +1761,8 @@ What the lint fence enforces today, in `eslint.config.mjs`: | Core importing a plugin backend | **Error** | 0 | - | | A plugin backend importing frontend code or `@/` | **Error** | 0 | - | | The shell importing plugin code | **Error** | 0 | - | -| A plugin frontend importing core through `@/` | Warning | 135 files | D1 | -| A plugin importing core by relative path | Warning | 67 files | D1 | +| A plugin frontend importing core through `@/` | Warning | 130 files | D1 | +| A plugin importing core by relative path | Warning | 66 files | D1 | | A plugin importing another plugin's source | Warning | 3 files | B18 | A warning does not fail a build, so the counts are held by @@ -1702,14 +1797,10 @@ Known specifics: **B6** moved file-manager off port 30004 onto `/plugin-api/file-manager/`, and **B7** moved tunnels off port 30003 onto `/plugin-api/tunnels/` and `/plugin-ws/tunnels/c2s/stream`. -- The terminal component and `TerminalTabContent` stay in core until the - terminal's Phase B step (session manager, split view). ssh-terminal - registers them through the legacy alias; **B6** switched every core caller - of the file manager over to `invokeAction("files.openHost"/"files.openEditor")` - instead of the `openFileManager`/`openFileInEditor` shell callbacks it - removed, so `TerminalWindow.tsx`'s embedding of the core terminal component - is now the only file-manager-adjacent legacy-core import left, tracked the - same way as any other plugin depending on an unconverted core component. +- **B9** moved the terminal, its session manager, the local terminal and the + command history panel into ssh-terminal, which imports nothing from core. + The file manager's terminal window renders the `terminal.view` component + instead of importing the core terminal. - Host data columns (`enableDocker`, `enableRdp`, `guacamoleConfig` and so on) stay in core types until Phase B moves them. Only UI branches on them left the shell. diff --git a/packages/plugin-sdk/FINISH-LIST.md b/packages/plugin-sdk/FINISH-LIST.md index 655dd5ed5..a3eb2f83e 100644 --- a/packages/plugin-sdk/FINISH-LIST.md +++ b/packages/plugin-sdk/FINISH-LIST.md @@ -144,3 +144,91 @@ build`'s esbuild step has no static-asset-copy pipeline the way core's Vite to ask Electron's main process for something, extend `BACKEND_REQUEST_HANDLERS` there rather than building a parallel channel. No owner needed unless a second caller appears. +- **B9 (ssh-terminal):** session sharing has no provider yet. The terminal + consumes `sessions.sharing` v1 (member joins through `authorizeJoin` and + `recordJoin`, collab room events through `subscribeRoom`/`unsubscribeRoom`) + and reads guest link resolution from `ctx.registry` as + `sessions.sharing.guests` (`resolve`, `recordJoin`), all typed in + `plugins/ssh-terminal/src/backend/services.ts`. Until something provides + them, share-link and room guests are refused and in-app joins fail. Core's + session-sharing, collab and open-tabs code reach the terminal through + `sessions.live` already. Owner: B12, which provides both from the + session-sharing plugin (the lookups are the old code in + `hosts/session-sharing` and `hosts/collab`: share repo, room repo, guest + rate limit, `canJoinRoomStageShare`, `session_sharing_globally_enabled`, + host `allowSessionSharing`, `getAuditUsername` for labels, `collabRoomHub`). + B12 also moves `eventsWsPath` in `collab/routes.ts` and the share dialog the + shell opens from a tab's `getShareTarget()`. +- **B9 (ssh-terminal), for D2:** the terminal socket is a public route with + `optionalAuth`, because share-link and room guests authenticate inside the + handler with their token. Guest auth itself is unchanged; review it with the + rest of the public routes. +- **B9 (ssh-terminal):** tmux has no provider yet. The terminal calls the + optional `tmux.sessions` v1 (`detect`, `attachOrCreate`, `waitForSession`) + and skips tmux attach without it (auto-tmux, the tmux monitor's attach, the + session picker). The toolbar's old host action list only ever held the + Tmux Monitor button, which is gone with it. `TmuxSessionPicker` lives in + ssh-terminal now. Owner: B10, which provides the service (the helpers are + `hosts/tmux/helper.ts`) and contributes its toolbar button to + `terminal.toolbar`. +- **B9 (ssh-terminal):** recording has no provider yet. The terminal calls the + optional `recordings.writer` v1 `open(meta)` and gets a sink with + `append(chunk)` (one call per 300 ms batch, the first starting with the + asciicast header), `persist(summary)` and `discard()`. Without it nothing is + recorded and no `session_recordings` row is written. Owner: B13, which + writes the file under `DATA_DIR/session_logs//.cast` (mkdir + and writeFile on the first append, appendFile after, never per chunk, see + issue #1049) and creates or updates the row as the old session manager did. +- **B9 (ssh-terminal):** `enable_terminal`, `enable_terminal_toolbar` and + `enable_command_history` are still live `ssh_data` columns. The copy into + ssh-terminal host settings shipped (`ssh-terminal-settings-migration.ts`) + and the terminal reads its settings, but core host routes, types, the + export/import sample and `HostEditorData` still carry the columns. + `enable_terminal_toolbar` also drives the RDP/VNC/Telnet toolbar + (`remote-desktop/GuacamoleApp.tsx`), whose host editor switch stays in the + General tab, so remote-desktop needs its own host setting and copy first. + Then drop all three in lockstep. Owner: remote-desktop's step for its copy, + then D0. +- **B9 (ssh-terminal):** the legacy `settings` rows for the terminal + (`terminal_session_timeout_minutes`, `terminal_session_persistence_enabled`, + `command_history_enabled`, `touch_input_settings`, `terminal_image_*`) are + left in place for a release after the move; nothing reads them. Delete them + in 3.0.0. `sessionPersistence` is carried over but, as before, nothing acts + on it. Owner: D0 to decide. +- **B9 (ssh-terminal):** the image storage paths used to be rejected at save + when not absolute; the schema-driven settings form accepts any string and + the resolver ignores a bad one with a warning. A custom field or a validate + hook on plugin settings would restore the save-time error. Owner: D1. +- **B9 (ssh-terminal):** `@termix/plugin-sdk/ui` now re-exports a handful of + core APIs the terminal needs (`logActivity`, `getHostPassword`, + `patchOpenTab`, `getUserPreferences`, `parseCustomKeybindings`, + `setHostAutoTmux`, `getCookie`) and the connection helpers. They belong on + the typed host bridge, which is also where B7's `app.logActivity` line + above lands. Owner: D1. +- **B9 (ssh-terminal):** `OPKSSHDialog` is exported from + `@termix/plugin-sdk/ui` so the terminal can show the opkssh sign-in. It + leaves the ui entry when opkssh becomes a plugin that draws its own UI in + `terminal.overlay`. Owner: Phase C (opkssh). +- **B9 (host-metrics):** the terminal toolbar's CPU/memory/disk bars + (`TerminalMetricsStatus`) poll the local backend only; on the desktop app a + host that lives on a connected remote server shows no bars. Same fix as the + B7 remote-origin line above. Owner: D1. +- **B9:** the Hosts panel's "Terminal" feature filter was removed along with + the column reads, the same as B7's tunnel filter. Owner: D1, with the + generic host-settings filter. +- **B9:** the startup purge of expired open tabs in `db/index.ts` read + `terminal_session_timeout_minutes` from core settings; it was removed and + expired tabs are dropped when the tab list is read, against the terminal's + own timeout (`GET /open-tabs/session-timeout`, 30 while the terminal is + off). No owner unless the table growth matters. +- **Pre-existing (B8), found in B9:** `node scripts/check-shell-plugin-ids.cjs` + fails on `src/ui/lib/host-to-ssh-host.ts` spelling `web-endpoint` (the + `pluginSettings` lookup B8 added). Not allowlisted and not introduced here. + Owner: D0 (read it through a registry or allowlist it with a reason). + +## Manual checks after 2.9.0 + +- SSH terminal: password auth, key auth (and an encrypted key's passphrase + prompt), TOTP keyboard-interactive, a jump host, Warpgate, split view, + snippets sent to a terminal, reconnect after a network drop and after a + page reload (session reattach), and the local terminal in the desktop app. diff --git a/packages/plugin-sdk/cli/lib/externals.mjs b/packages/plugin-sdk/cli/lib/externals.mjs index 2c11a5e27..5c2f27bbc 100644 --- a/packages/plugin-sdk/cli/lib/externals.mjs +++ b/packages/plugin-sdk/cli/lib/externals.mjs @@ -23,6 +23,8 @@ export const BACKEND_EXTERNALS = [ "@anthropic-ai/sdk", "drizzle-orm", "drizzle-orm/*", + // Native image processing, loaded on first use by the terminal's image upload. + "sharp", ]; /** diff --git a/packages/plugin-sdk/src/backend.ts b/packages/plugin-sdk/src/backend.ts index c1bf7526e..2216995b6 100644 --- a/packages/plugin-sdk/src/backend.ts +++ b/packages/plugin-sdk/src/backend.ts @@ -292,6 +292,16 @@ export interface PluginWebSocketConnection { readonly request: unknown; /** The ws WebSocket. Binary frames and backpressure work as usual. */ readonly socket: unknown; + /** The client address, behind a trusted proxy when one is configured. */ + readonly clientIp: string; + /** The public origin the request came in on, for browser redirects. */ + readonly requestOrigin: string; + /** + * Whether the user's data key is still unlocked. False for a guest and + * once a session expires, so a long-lived socket can refuse work it can no + * longer decrypt for. + */ + isDataUnlocked: () => boolean; } export type PluginWebSocketHandler = ( @@ -305,6 +315,12 @@ export interface PluginWebSocketOptions { * guest or a token-in-query protocol needs. */ public?: boolean; + /** + * With `public`, still resolves a token when the client sent one, so one + * route can serve signed-in users and a guest arriving with a token of its + * own. The handler gets the user id, or an empty one for a guest. + */ + optionalAuth?: boolean; } /** @@ -551,6 +567,22 @@ export interface PluginHosts { listUsers: () => Promise; /** Non-system roles the caller may pick as a share target. */ listRoles: () => Promise; + /** + * Marks a live session on a host, for the online indicator. Counted with + * every other feature's sessions; call the returned function once when the + * session ends. Needs hosts:read. + */ + trackSession: (hostId: number) => () => void; + /** + * Adds a recent activity entry for the acting user, rate limited and + * refused for a host they cannot reach, like the dashboard's own route. + * Needs hosts:read. + */ + recordActivity: ( + hostId: number, + type: string, + hostName: string, + ) => Promise; } /** @@ -579,7 +611,8 @@ export type PluginSshPurpose = | "remote-desktop" | "tunnel" | "file-manager" - | "file-transfer"; + | "file-transfer" + | "terminal"; export type PluginSshAuthOutcome = | { status: "ready" } @@ -717,6 +750,18 @@ export interface PluginSsh { /** The pool key withConnection would use, for clearing it. */ poolKey: (pool: string, host: PluginSshHost) => string; + /** + * The host as the acting user may connect to it: RBAC, owner-key + * decryption, shared-host overrides and external secret references + * resolved, secrets included. By sync id when given, since a numeric id + * only names a host on the database that issued it. Null when the host is + * not on this server or the user cannot reach it. + */ + resolveHost: ( + hostId: number, + options?: { syncId?: string | null }, + ) => Promise; + /** * Lower level: fills an ssh2 config for a client the plugin drives itself, * for transports with their own prompt flow. Never connects. @@ -729,18 +774,49 @@ export interface PluginSsh { /** Server-side host id when it differs from host.id. */ serverHostId?: number; log?: (level: "info" | "warning" | "error", message: string) => void; + /** A person is at the keyboard, so providers may prompt. */ + interactive?: boolean; + /** + * The ws socket the host key verifier asks on when a key is new or has + * changed. Without one a changed key is refused. + */ + hostKeySocket?: unknown; }, - ) => Promise<{ - config: Record; - outcome: PluginSshAuthOutcome; - }>; + ) => Promise; /** Lower level: port knocking, jump hosts or proxy; sets config.sock. */ openTransport: ( host: PluginSshHost, config: Record, + options?: { + /** False when the caller already resolved DNS, or a jump host will. */ + resolveDns?: boolean; + log?: (level: "info" | "warning" | "error", message: string) => void; + }, ) => Promise<{ jumpClient: unknown | null; via: string }>; + /** + * Starts the browser step behind an `_auth_required` message. + * The provider for the host's own auth type wins, else the first provider + * that owns the interaction. Rejects with PluginSshInteractionError when + * none can. + */ + startInteraction: ( + interaction: string, + request: { + hostId: number; + socket: unknown; + requestOrigin: string; + payload?: Record; + }, + ) => Promise; + + /** Cancels a pending browser step on every provider that owns it. */ + cancelInteraction: ( + interaction: string, + request: { hostId?: number; requestId?: string }, + ) => Promise; + /** What a keyboard-interactive round is. Pure; no capability needed. */ classifyKeyboardInteractive: ( round: { @@ -764,6 +840,31 @@ export interface PluginSsh { supportsBackground: (authType: string) => boolean; } +/** What ctx.ssh.prepare built, plus the provider hooks bound to it. */ +export interface PluginSshPrepared { + config: Record; + outcome: PluginSshAuthOutcome; + /** The auth type whose provider prepared it, null when none exists. */ + authType: string | null; + /** The provider's banner hook, bound to this host and connection. */ + onBanner?: (banner: string) => PluginSshBannerDecision | undefined; + /** The provider's auth failure hook, bound to this host and connection. */ + onAuthFailed?: (context: { + error: Error; + retries: number; + canRetry: boolean; + methodNotAvailable: boolean; + }) => PluginSshAuthOutcome | undefined; +} + +/** Thrown by ctx.ssh.startInteraction when no provider can start it. */ +export class PluginSshInteractionError extends Error { + constructor(message: string) { + super(message); + this.name = "PluginSshInteractionError"; + } +} + export interface PluginSshAuthEnv { /** The ssh2 Client about to connect; certificate auth patches it. */ client: unknown; @@ -845,6 +946,12 @@ export interface PluginSshAuthProvider { requestOrigin: string; payload: Record; }) => Promise; + /** Cancels a pending browser step. */ + cancelInteraction?: (request: { + userId: string; + hostId?: number; + requestId?: string; + }) => void | Promise; } /** @@ -986,6 +1093,23 @@ export interface PluginDesktop { ) => Promise<{ success: true }>; } +/** + * An audit line under an action name of the plugin's choosing (an SSH login), + * for the acting user, next to the plugin_* line every privileged ctx call + * already writes. + */ +export interface PluginAudit { + record: (entry: { + action: string; + resourceType?: string; + resourceId?: string; + resourceName?: string; + details?: string; + success: boolean; + errorMessage?: string; + }) => Promise; +} + export interface PluginContext { readonly pluginId: string; readonly manifest: PluginManifest; @@ -1010,6 +1134,8 @@ export interface PluginContext { readonly auth: PluginAuth; /** Opens Electron windows outside the main renderer. Needs desktop:window. */ readonly desktop: PluginDesktop; + /** Audit lines under the plugin's own action names. */ + readonly audit: PluginAudit; /** * Runs `fn` with `userId` as the acting user, for background work that has diff --git a/packages/plugin-sdk/src/frontend.ts b/packages/plugin-sdk/src/frontend.ts index e2122ba49..875326d05 100644 --- a/packages/plugin-sdk/src/frontend.ts +++ b/packages/plugin-sdk/src/frontend.ts @@ -49,6 +49,13 @@ export interface PluginTabRecord { label: string; host?: PluginHostRecord; data?: Record; + /** Stable across restores, for a session tab to reattach by. */ + instanceId?: string; + /** A backend session a session tab should reattach to. */ + restoredSessionId?: string | null; + /** A live shared session this tab joins instead of connecting its own. */ + joinSharedSessionId?: string | null; + joinShareId?: string | null; [key: string]: unknown; } @@ -76,7 +83,11 @@ export interface ShellApi { openSingletonTab: (type: string, options?: OpenTabOptions) => void; closeTab: (tabId: string) => void; renameTab: (tabId: string, label: string) => void; - openTerminalTab: (host: PluginHostRecord, path?: string) => void; + /** Saves a quick-connect tab's host as a real host. Absent in some shells. */ + saveQuickConnect?: ( + tab: PluginTabRecord, + host: PluginHostRecord, + ) => Promise; /** Opens a rail view in the left sidebar. */ openRailView: (id: string) => void; /** Closes a rail view wherever it is shown. */ @@ -141,6 +152,11 @@ export interface RailItemContribution { } export interface PanelProps { + /** + * The command-target tab (see TabOptions.commandTarget) the user is working + * in: the active one, else the one focused last. For panels that act on it. + */ + targetTab?: PluginTabRecord; /** Whether the panel is the one currently shown. */ active: boolean; shell: ShellApi; @@ -157,6 +173,33 @@ export interface PanelOptions { keepMounted?: boolean; } +/** + * What a session tab hands the shell through `handleRef`, so split view, tab + * close and reconnect-all can drive it without knowing what kind it is. + */ +export interface TabHandle { + focus?: () => void; + fit?: () => void; + reconnect?: () => void; + disconnect?: () => void; + isConnected?: () => boolean; + sendInput?: (data: string) => void; + paste?: (text: string) => void; + refresh?: () => void; + notifyResize?: () => void; + /** A tab that shows its own share dialog. */ + canShare?: () => boolean; + openShareModal?: () => void; + /** A tab whose session the shell's share dialog can share. */ + getShareTarget?: () => { + hostId: number; + sessionId: string; + protocol: "ssh"; + tabInstanceId?: string; + } | null; + [key: string]: unknown; +} + export interface TabProps { tab: PluginTabRecord; host?: PluginHostRecord; @@ -203,6 +246,15 @@ export interface TabOptions { panelFrame?: boolean; /** False keeps the tab out of saved layouts and workspaces. Default true. */ inLayouts?: boolean; + /** + * Its session takes typed commands: the history, macros and SSH tools + * panels act on the one focused last. + */ + commandTarget?: boolean; + /** Paints its own background, so the shell leaves its frame transparent. */ + ownBackground?: boolean; + /** Every open is a new tab, labelled " (2)" and so on. */ + multiInstance?: boolean; /** Warms the tab's code before it is opened. */ preload?: () => Promise<unknown>; } @@ -504,6 +556,16 @@ export interface TermixApp extends TermixAppInfo { contribution: SlotContribution, ) => Disposer; invokeAction: (id: string, ...args: unknown[]) => Promise<unknown>; + /** + * Offers a component to other plugins and to core by id, rendered where + * they choose with `PluginComponent` from @termix/plugin-sdk/ui or + * `usePluginComponent`. The id should start with a name the plugin owns + * ("terminal.view"). The props are the owner's contract; document them. + */ + registerComponent: ( + id: string, + component: ComponentType<Record<string, unknown>>, + ) => Disposer; registerSshAuthEditor: (editor: SshAuthEditorContribution) => Disposer; registerLoginMethod: (method: LoginMethodContribution) => Disposer; registerSecondFactorUI: (factor: SecondFactorContribution) => Disposer; @@ -609,7 +671,13 @@ export interface PluginHostBridge { useTabs: () => TabsApi; invokeAction: (id: string, ...args: unknown[]) => Promise<unknown>; useSshAuthTypes: () => { types: SshAuthTypeInfo[]; loaded: boolean }; - useSlotContributions: (slotId: string) => SlotContribution[]; + useSlotContributions: ( + slotId: string, + context?: Record<string, unknown>, + ) => SlotContribution[]; + usePluginComponent: ( + id: string, + ) => ComponentType<Record<string, unknown>> | undefined; } let host: PluginHostBridge | null = null; @@ -717,10 +785,24 @@ export function useSshAuthTypes(): { * metadata (id, titleKey, component). For an owner that needs to build a * catalog from what was contributed rather than just render it in place, e.g. * host-metrics listing manager cards plugins added to "host-metrics.managers" - * next to its own. Same permission and `when` filtering as ComponentSlot. + * next to its own. Same permission and `when` filtering as ComponentSlot; + * `context` is what each contribution's `when` sees. */ -export function useSlotContributions(slotId: string): SlotContribution[] { - return requireHost().useSlotContributions(slotId); +export function useSlotContributions( + slotId: string, + context?: Record<string, unknown>, +): SlotContribution[] { + return requireHost().useSlotContributions(slotId, context); +} + +/** + * The component another plugin registered under `id` with + * app.registerComponent, or undefined while that plugin is off. + */ +export function usePluginComponent( + id: string, +): ComponentType<Record<string, unknown>> | undefined { + return requireHost().usePluginComponent(id); } /** diff --git a/packages/plugin-sdk/src/testing.ts b/packages/plugin-sdk/src/testing.ts index 53d5c3061..5eaa2751a 100644 --- a/packages/plugin-sdk/src/testing.ts +++ b/packages/plugin-sdk/src/testing.ts @@ -62,6 +62,8 @@ export interface FakeContextOptions { /** Users and roles ctx.hosts.listUsers/listRoles answer with. */ shareableUsers?: PluginShareableUser[]; shareableRoles?: PluginShareableRole[]; + /** Hosts ctx.ssh.resolveHost answers with, secrets included. */ + sshHosts?: PluginSshHost[]; } export interface FakeAuthRegistrations { @@ -116,6 +118,18 @@ export interface FakePluginContext { title?: string; ignoreCert?: boolean; }>; + /** Every ctx.audit.record entry, in order. */ + audits: Array<{ action: string; success: boolean; [key: string]: unknown }>; + /** Every ctx.hosts.recordActivity call, in order. */ + activities: Array<{ hostId: number; type: string; hostName: string }>; + /** Host ids with a live ctx.hosts.trackSession, one entry per session. */ + trackedSessions: number[]; + /** Every ctx.ssh.startInteraction / cancelInteraction call, in order. */ + interactions: Array<{ + action: "start" | "cancel"; + interaction: string; + request: Record<string, unknown>; + }>; /** Changes the acting user, as core's request middleware would. */ setActor: (userId: string | undefined) => void; /** Implementations provided through ctx.services.provide, by service name. */ @@ -187,6 +201,10 @@ export function createFakeContext( const sshConnections: FakePluginContext["sshConnections"] = []; const hostShares: FakePluginContext["hostShares"] = []; const desktopWindows: FakePluginContext["desktopWindows"] = []; + const audits: FakePluginContext["audits"] = []; + const activities: FakePluginContext["activities"] = []; + const trackedSessions: number[] = []; + const interactions: FakePluginContext["interactions"] = []; const hostsById = new Map<number, PluginHostSummary>( (options.hosts ?? []).map((h) => [h.id, h]), ); @@ -486,6 +504,25 @@ export function createFakeContext( }, listUsers: async () => options.shareableUsers ?? [], listRoles: async () => options.shareableRoles ?? [], + trackSession: (hostId) => { + trackedSessions.push(hostId); + let active = true; + return () => { + if (!active) return; + active = false; + const index = trackedSessions.indexOf(hostId); + if (index >= 0) trackedSessions.splice(index, 1); + }; + }, + recordActivity: async (hostId, type, hostName) => { + activities.push({ hostId, type, hostName }); + }, + }, + + audit: { + record: async (entry) => { + audits.push({ ...entry }); + }, }, ssh: { @@ -512,8 +549,24 @@ export function createFakeContext( }), poolKey: (pool, host) => `${pool}:${host.userId}:${host.ip}:${host.port}:${host.username}`, - prepare: async () => ({ config: {}, outcome: { status: "ready" } }), + resolveHost: async (hostId, resolveOptions) => + (options.sshHosts ?? []).find((host) => + resolveOptions?.syncId + ? host.syncId === resolveOptions.syncId + : host.id === hostId, + ) ?? null, + prepare: async () => ({ + config: {}, + outcome: { status: "ready" }, + authType: null, + }), openTransport: async () => ({ jumpClient: null, via: "direct" }), + startInteraction: async (interaction, request) => { + interactions.push({ action: "start", interaction, request }); + }, + cancelInteraction: async (interaction, request) => { + interactions.push({ action: "cancel", interaction, request }); + }, classifyKeyboardInteractive: ({ prompts }, host) => ({ kind: "auto", responses: prompts.map((p) => @@ -586,6 +639,10 @@ export function createFakeContext( hostShares, auth, desktopWindows, + audits, + activities, + trackedSessions, + interactions, setActor: (userId) => { actor = userId; }, @@ -616,6 +673,8 @@ export interface MockContextOptions { permissions?: string[]; /** Hosts ctx.hosts.list/get/checkAccess serve. See FakeContextOptions. */ hosts?: PluginHostSummary[]; + /** Hosts ctx.ssh.resolveHost serves. See FakeContextOptions. */ + sshHosts?: PluginSshHost[]; } export interface MockPluginContext extends FakePluginContext { @@ -650,6 +709,7 @@ export function createMockCtx( router: options.router, permissions: options.permissions, hosts: options.hosts, + sshHosts: options.sshHosts, manifest: { capabilities: options.capabilities ?? [], ...options.manifest, @@ -795,6 +855,14 @@ export function createMockCtx( require("hosts:write"); return ctx.hosts.listRoles(); }, + trackSession: (hostId) => { + require("hosts:read"); + return ctx.hosts.trackSession(hostId); + }, + recordActivity: async (hostId, type, hostName) => { + require("hosts:read"); + return ctx.hosts.recordActivity(hostId, type, hostName); + }, }, ssh: { @@ -814,9 +882,23 @@ export function createMockCtx( require("credentials:use"); return ctx.ssh.prepare(host, prepareOptions); }, - openTransport: async (host, config) => { + openTransport: async (host, config, transportOptions) => { require("ssh:connect"); - return ctx.ssh.openTransport(host, config); + return ctx.ssh.openTransport(host, config, transportOptions); + }, + resolveHost: async (hostId, resolveOptions) => { + require("ssh:connect"); + require("credentials:use"); + return ctx.ssh.resolveHost(hostId, resolveOptions); + }, + startInteraction: async (interaction, request) => { + require("ssh:connect"); + require("credentials:use"); + return ctx.ssh.startInteraction(interaction, request); + }, + cancelInteraction: async (interaction, request) => { + require("ssh:connect"); + return ctx.ssh.cancelInteraction(interaction, request); }, jumpChain: async (jumpHosts, chainOptions) => { require("ssh:connect"); diff --git a/packages/plugin-sdk/src/testing/setup.ts b/packages/plugin-sdk/src/testing/setup.ts index efac467ce..11e3e5fb9 100644 --- a/packages/plugin-sdk/src/testing/setup.ts +++ b/packages/plugin-sdk/src/testing/setup.ts @@ -112,6 +112,7 @@ const offline = () => loaded: true, }), useSlotContributions: () => [], + usePluginComponent: () => undefined, }; afterEach(() => { diff --git a/plugins/ai/manifest.json b/plugins/ai/manifest.json index 6a42c742b..b7c3fd5a6 100644 --- a/plugins/ai/manifest.json +++ b/plugins/ai/manifest.json @@ -33,7 +33,8 @@ "workspaces": "^1.0.0", "network-topology": "^1.0.0", "snippets": "^1.0.0", - "fleets": "^1.0.0" + "fleets": "^1.0.0", + "ssh-terminal": "^1.0.0" }, "requires": [ { @@ -55,6 +56,11 @@ "service": "fleets.access", "versionRange": "^1.0.0", "optional": true + }, + { + "service": "terminal.history", + "versionRange": "^1.0.0", + "optional": true } ], "providesSecret": [ diff --git a/plugins/ai/src/backend/services.ts b/plugins/ai/src/backend/services.ts index 163b58aa3..3a8619e09 100644 --- a/plugins/ai/src/backend/services.ts +++ b/plugins/ai/src/backend/services.ts @@ -221,3 +221,30 @@ export async function deleteSnippet( ): Promise<boolean> { return requireSnippetsAccess(userId).remove(id); } + +/** The terminal's command history, as ctx.services.get("terminal.history") returns it. */ +interface TerminalHistory { + list: ( + hostId: number, + limit?: number, + ) => Promise<Array<{ command: string; executedAt: string }>>; +} + +/** + * The user's recent commands on one host, or null when the ssh-terminal + * plugin is off or the user may not read its history. + */ +export async function listCommandHistory( + userId: string, + hostId: number, + limit: number, +): Promise<Array<{ command: string; executedAt: string }> | null> { + if (!current) return null; + try { + return await current + .get<TerminalHistory>("terminal.history", { userId }) + .list(hostId, limit); + } catch { + return null; + } +} diff --git a/plugins/ai/src/backend/tools/read-tools.ts b/plugins/ai/src/backend/tools/read-tools.ts index d9d3d4552..5b1c3c285 100644 --- a/plugins/ai/src/backend/tools/read-tools.ts +++ b/plugins/ai/src/backend/tools/read-tools.ts @@ -1,7 +1,6 @@ import { createCurrentNotificationChannelRepository, createCurrentAutomationRepository, - createCurrentCommandHistoryRepository, createCurrentHomepageItemRepository, createCurrentHostRepository, } from "../../../../../src/backend/database/repositories/factory.js"; @@ -11,6 +10,7 @@ import { getNetworkTopology, listSnippets, listFleets, + listCommandHistory, } from "../services.js"; /** @@ -244,13 +244,9 @@ export const readTools: AiTool[] = [ ); if (!host) return { error: "Host not found" }; - const commands = - await createCurrentCommandHistoryRepository().listCommandsForHost( - context.userId, - hostId, - limit, - ); - return { commands }; + const history = await listCommandHistory(context.userId, hostId, limit); + if (history === null) return { commands: [], unavailable: true }; + return { commands: history.map((entry) => entry.command) }; }, }, { diff --git a/plugins/ai/src/frontend/index.tsx b/plugins/ai/src/frontend/index.tsx index 8939bd071..f720f44b8 100644 --- a/plugins/ai/src/frontend/index.tsx +++ b/plugins/ai/src/frontend/index.tsx @@ -6,9 +6,9 @@ import type { TermixApp, } from "@termix/plugin-sdk/frontend"; import type { - TerminalDockProps, + TerminalSidePanelProps, TerminalSlotApi, -} from "@/features/terminal/terminal-slots"; +} from "./terminal/terminal-slot-types"; import { AiPanel } from "./AiPanel"; import { TerminalAiPanel } from "./terminal/TerminalAiPanel"; import { AiAssistantStep } from "./AiAssistantStep"; @@ -18,7 +18,7 @@ import { getAiStatus } from "./ai-api"; import { AI_STATUS_CHANGED_EVENT } from "./use-ai-availability"; const ASSISTANT_ACTION = "ai.openWithContext"; -const DOCK_ID = "ai.assistant"; +const SIDE_PANEL_ID = "ai.assistant"; function AssistantPanel({ activeTabType }: PanelProps) { return ( @@ -32,15 +32,15 @@ function AssistantTab() { return <AiPanel />; } -/** The assistant docked beside a terminal. */ -function TerminalDock({ +/** The assistant beside a terminal, in its "terminal.sidePanel" slot. */ +function TerminalSidePanel({ host, hostId, hostLabel, - dockProps, + panelProps, onClose, onRunInTerminal, -}: TerminalDockProps) { +}: TerminalSidePanelProps) { if (!hostId) return null; return ( <TerminalAiPanel @@ -48,7 +48,7 @@ function TerminalDock({ hostId={hostId} activeTab={`terminal:${host?.name || host?.ip || ""}`} initialContext={ - typeof dockProps.context === "string" ? dockProps.context : "" + typeof panelProps.context === "string" ? panelProps.context : "" } onClose={onClose} onRunInTerminal={onRunInTerminal} @@ -114,12 +114,12 @@ export function activate(app: TermixApp): void { }), ); surface.push( - app.registerSlotContribution("terminal.dock", { - actionId: DOCK_ID, + app.registerSlotContribution("terminal.sidePanel", { + actionId: SIDE_PANEL_ID, titleKey: "ai.assistant", icon: Bot, kind: "component", - component: TerminalDock as unknown as ComponentType< + component: TerminalSidePanel as unknown as ComponentType< Record<string, unknown> >, when: offeredOnHost, @@ -165,7 +165,9 @@ export function activate(app: TermixApp): void { app.registerAction( ASSISTANT_ACTION, (terminal: TerminalSlotApi) => - terminal?.openDock(DOCK_ID, { context: terminal.getBufferText() }), + terminal?.openSidePanel(SIDE_PANEL_ID, { + context: terminal.getBufferText(), + }), { permission: "services.use" }, ); diff --git a/plugins/ai/src/frontend/terminal/TerminalAiPanel.tsx b/plugins/ai/src/frontend/terminal/TerminalAiPanel.tsx index a14bf3c65..cca691cd5 100644 --- a/plugins/ai/src/frontend/terminal/TerminalAiPanel.tsx +++ b/plugins/ai/src/frontend/terminal/TerminalAiPanel.tsx @@ -18,7 +18,7 @@ import { persistAiPanelPosition, readStoredAiPanelPosition, type ToolbarPosition, -} from "@/features/terminal/toolbar-geometry"; +} from "./panel-geometry"; interface HistoryEntry { role: "user" | "assistant"; diff --git a/plugins/ai/src/frontend/terminal/panel-geometry.ts b/plugins/ai/src/frontend/terminal/panel-geometry.ts new file mode 100644 index 000000000..a53988d01 --- /dev/null +++ b/plugins/ai/src/frontend/terminal/panel-geometry.ts @@ -0,0 +1,90 @@ +/** + * Where the docked assistant panel sits over a terminal. Same clamping rules + * as the terminal's own toolbar, kept here so the panel owns its geometry. + */ + +export interface ToolbarPosition { + x: number; + y: number; +} + +export interface RectLike { + left: number; + right: number; + top: number; + bottom: number; + width: number; + height: number; +} + +const AI_PANEL_POSITION_STORAGE_KEY = "termix-terminal-ai-panel-position"; +const TOOLBAR_MARGIN = 8; +const RECOVERY_SIZE = 44; + +function sanitizeToolbarPosition(value: unknown): ToolbarPosition { + if (!value || typeof value !== "object") return { x: 0, y: 0 }; + const candidate = value as Partial<ToolbarPosition>; + return Number.isFinite(candidate.x) && Number.isFinite(candidate.y) + ? { x: Number(candidate.x), y: Number(candidate.y) } + : { x: 0, y: 0 }; +} + +export function readStoredAiPanelPosition(): ToolbarPosition { + if (typeof window === "undefined") return { x: 0, y: 0 }; + try { + return sanitizeToolbarPosition( + JSON.parse( + window.localStorage.getItem(AI_PANEL_POSITION_STORAGE_KEY) ?? "null", + ), + ); + } catch { + return { x: 0, y: 0 }; + } +} + +export function persistAiPanelPosition(position: ToolbarPosition): void { + try { + window.localStorage.setItem( + AI_PANEL_POSITION_STORAGE_KEY, + JSON.stringify(sanitizeToolbarPosition(position)), + ); + } catch { + // Storage may be unavailable in hardened browser contexts. + } +} + +export function clampToolbarPosition( + position: ToolbarPosition, + toolbarRect: RectLike, + hostRect: RectLike, + renderedPosition: ToolbarPosition = position, +): ToolbarPosition { + if ( + !toolbarRect.width || + !toolbarRect.height || + !hostRect.width || + !hostRect.height + ) + return sanitizeToolbarPosition(position); + const left = hostRect.left + TOOLBAR_MARGIN; + const right = hostRect.right - TOOLBAR_MARGIN; + const top = hostRect.top + TOOLBAR_MARGIN; + const bottom = hostRect.bottom - TOOLBAR_MARGIN; + const baseLeft = toolbarRect.left - renderedPosition.x; + const baseRight = toolbarRect.right - renderedPosition.x; + const baseTop = toolbarRect.top - renderedPosition.y; + const baseBottom = toolbarRect.bottom - renderedPosition.y; + const clamp = (min: number, max: number, value: number) => + Math.min(max, Math.max(min, value)); + const xMin = + toolbarRect.width <= right - left + ? left - baseLeft + : left + RECOVERY_SIZE - baseRight; + const xMax = right - baseRight; + const yMin = + toolbarRect.height <= bottom - top + ? top - baseTop + : top + RECOVERY_SIZE - baseBottom; + const yMax = bottom - baseBottom; + return { x: clamp(xMin, xMax, position.x), y: clamp(yMin, yMax, position.y) }; +} diff --git a/plugins/ai/src/frontend/terminal/terminal-slot-types.ts b/plugins/ai/src/frontend/terminal/terminal-slot-types.ts new file mode 100644 index 000000000..e1f9160ea --- /dev/null +++ b/plugins/ai/src/frontend/terminal/terminal-slot-types.ts @@ -0,0 +1,31 @@ +/** + * What the ssh-terminal plugin hands its slots, as that plugin documents it. + * Kept as a local copy: a plugin never imports another plugin's source. + */ + +interface TerminalHost { + id: number | string; + name?: string; + ip?: string; + enableAiAssistant?: boolean; + [key: string]: unknown; +} + +/** What a "terminal.toolbar" action is invoked with. */ +export interface TerminalSlotApi { + host: TerminalHost | undefined; + getBufferText: () => string; + sessionId: () => string | null; + openSidePanel: (panelId: string, props?: Record<string, unknown>) => void; + runCommand: (command: string) => void; +} + +/** What a "terminal.sidePanel" component is rendered with. */ +export interface TerminalSidePanelProps { + host: TerminalHost | undefined; + hostId: number | undefined; + hostLabel: string; + panelProps: Record<string, unknown>; + onClose: () => void; + onRunInTerminal: (command: string) => void; +} diff --git a/plugins/ai/tests/backend/command-history-service.test.ts b/plugins/ai/tests/backend/command-history-service.test.ts new file mode 100644 index 000000000..426eedfce --- /dev/null +++ b/plugins/ai/tests/backend/command-history-service.test.ts @@ -0,0 +1,39 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { + listCommandHistory, + setPluginServices, +} from "../../src/backend/services.js"; + +afterEach(() => setPluginServices(null)); + +describe("listCommandHistory", () => { + it("reads the user's history through terminal.history", async () => { + const list = vi.fn(async () => [ + { command: "ls", executedAt: "2026-01-01T00:00:00.000Z" }, + ]); + const get = vi.fn(() => ({ list })); + setPluginServices({ get, provide: vi.fn() } as never); + + const history = await listCommandHistory("user-1", 4, 25); + + expect(get).toHaveBeenCalledWith("terminal.history", { userId: "user-1" }); + expect(list).toHaveBeenCalledWith(4, 25); + expect(history).toEqual([ + { command: "ls", executedAt: "2026-01-01T00:00:00.000Z" }, + ]); + }); + + it("answers null while the terminal is off or refuses the user", async () => { + expect(await listCommandHistory("user-1", 4, 25)).toBeNull(); + + setPluginServices({ + get: () => ({ + list: async () => { + throw new Error("unavailable"); + }, + }), + provide: vi.fn(), + } as never); + expect(await listCommandHistory("user-1", 4, 25)).toBeNull(); + }); +}); diff --git a/plugins/docker/src/frontend/components/ConsoleTerminal.tsx b/plugins/docker/src/frontend/components/ConsoleTerminal.tsx index 23c85d56c..8ff461cf3 100644 --- a/plugins/docker/src/frontend/components/ConsoleTerminal.tsx +++ b/plugins/docker/src/frontend/components/ConsoleTerminal.tsx @@ -19,9 +19,9 @@ import { useTranslation, useConnectionRetry, } from "@termix/plugin-sdk/frontend"; -import { resolveTermixThemeColors } from "@/features/terminal/terminal-theme"; +import { resolveTermixThemeColors } from "@/lib/terminal-look/terminal-theme"; import { DEFAULT_TERMINAL_CONFIG, TERMINAL_FONTS } from "@/lib/terminal-themes"; -import { ensureTerminalFontsLoaded } from "@/features/terminal/terminal-global-styles"; +import { ensureTerminalFontsLoaded } from "@/lib/terminal-look/terminal-global-styles"; import { useTheme } from "@/components/theme-provider"; import { ConnectionScreen } from "@/components/connection/ConnectionScreen.tsx"; import { diff --git a/plugins/file-manager/src/frontend/components/TerminalWindow.tsx b/plugins/file-manager/src/frontend/components/TerminalWindow.tsx index 5d383b2e8..80258b267 100644 --- a/plugins/file-manager/src/frontend/components/TerminalWindow.tsx +++ b/plugins/file-manager/src/frontend/components/TerminalWindow.tsx @@ -1,13 +1,9 @@ import React from "react"; import { DraggableWindow } from "./DraggableWindow.tsx"; -import { - Terminal, - type TerminalHandle, - type TerminalHostConfig, -} from "@/features/terminal/Terminal.tsx"; +import type { TabHandle as TerminalHandle } from "@termix/plugin-sdk/frontend"; +import { PluginComponent } from "@termix/plugin-sdk/ui"; import { useWindowManager } from "./WindowManager.tsx"; import { useTranslation } from "react-i18next"; -import { CommandHistoryProvider } from "@/features/terminal/command-history/CommandHistoryContext.tsx"; import type { SSHHost } from "@/types/index.ts"; import { ExternalLink } from "lucide-react"; @@ -96,7 +92,7 @@ export function TerminalWindow({ : t("terminal.terminalTitle", { host: hostConfig.name }); return ( - <CommandHistoryProvider> + <> <DraggableWindow title={terminalTitle} initialX={initialX} @@ -126,15 +122,16 @@ export function TerminalWindow({ ) : null } > - <Terminal - ref={terminalRef} - hostConfig={hostConfig as TerminalHostConfig} + <PluginComponent + id="terminal.view" + handleRef={terminalRef} + hostConfig={hostConfig} isVisible={!currentWindow.isMinimized} initialPath={initialPath} executeCommand={executeCommand} onClose={handleClose} /> </DraggableWindow> - </CommandHistoryProvider> + </> ); } diff --git a/plugins/file-manager/src/frontend/index.tsx b/plugins/file-manager/src/frontend/index.tsx index 8a077c3f3..8ba31b5fb 100644 --- a/plugins/file-manager/src/frontend/index.tsx +++ b/plugins/file-manager/src/frontend/index.tsx @@ -1,10 +1,11 @@ import type { ComponentType } from "react"; import { FolderSearch, ArrowLeftRight } from "lucide-react"; -import type { - PluginHostRecord, - StandaloneViewProps, - TabProps, - TermixApp, +import { + invokeAction, + type PluginHostRecord, + type StandaloneViewProps, + type TabProps, + type TermixApp, } from "@termix/plugin-sdk/frontend"; import { GRID_SIZE } from "@/types/homepage-types"; import { FileManager } from "./FileManager.tsx"; @@ -15,7 +16,7 @@ import { FileManagerWidget } from "./homepage/FileManagerWidget.tsx"; import { FileManagerWidgetEditForm } from "./homepage/FileManagerWidgetEditForm.tsx"; import { startTransferMonitor } from "./TransferMonitor.tsx"; -function FilesTab({ tab, host, sshHost, isVisible, shell }: TabProps) { +function FilesTab({ tab, host, sshHost, isVisible }: TabProps) { const data = tab.data as { initialFilePath?: string; initialPath?: string } | undefined; return ( @@ -25,7 +26,9 @@ function FilesTab({ tab, host, sshHost, isVisible, shell }: TabProps) { initialPath={data?.initialPath} isVisible={isVisible} onOpenTerminalTab={ - host ? (path) => shell.openTerminalTab(host, path) : undefined + host + ? (path) => void invokeAction("terminal.open", host, { path }) + : undefined } /> ); diff --git a/plugins/host-metrics/locales/en.json b/plugins/host-metrics/locales/en.json index db34e9642..254edf3cc 100644 --- a/plugins/host-metrics/locales/en.json +++ b/plugins/host-metrics/locales/en.json @@ -239,5 +239,8 @@ "showCurrentValue": "Show Current Value", "chartMetric": "Metric", "metricRange": "Range" + }, + "terminalStatus": { + "unavailable": "Live stats unavailable" } } diff --git a/plugins/host-metrics/src/frontend/TerminalMetricsStatus.tsx b/plugins/host-metrics/src/frontend/TerminalMetricsStatus.tsx new file mode 100644 index 000000000..259ee546d --- /dev/null +++ b/plugins/host-metrics/src/frontend/TerminalMetricsStatus.tsx @@ -0,0 +1,174 @@ +import { useEffect, useState } from "react"; +import { usePluginApi, useTranslation } from "@termix/plugin-sdk/frontend"; +import { + cn, + getPollingEnvironmentMultiplier, + runAdaptivePolling, +} from "@termix/plugin-sdk/ui"; + +/** What the ssh-terminal plugin hands a "terminal.toolbarStatus" component. */ +interface TerminalToolbarStatusProps { + host: { id: number | string }; + isConnected: boolean; + /** Visible on a desktop viewport and connected: poll only while true. */ + active: boolean; +} + +interface Sample { + cpu: number | null; + memory: number | null; + disk: number | null; +} + +function StatBar({ + label, + percent, +}: { + label: string; + percent: number | null; +}) { + const value = percent == null ? null : Math.max(0, Math.min(100, percent)); + return ( + <div className="flex items-center gap-1.5 px-1.5"> + <span className="w-12 shrink-0 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground"> + {label} + </span> + <div className="h-1.5 w-12 shrink-0 overflow-hidden rounded-full bg-muted"> + {value != null && ( + <div + className={cn( + "h-full rounded-full transition-all", + value >= 90 + ? "bg-red-500" + : value >= 70 + ? "bg-yellow-500" + : "bg-accent-brand", + )} + style={{ width: `${value}%` }} + /> + )} + </div> + <span className="w-7 shrink-0 text-right text-[10px] tabular-nums text-muted-foreground"> + {value == null ? "--" : `${Math.round(value)}%`} + </span> + </div> + ); +} + +/** CPU, memory and disk bars in the terminal toolbar's expanded view. */ +export function TerminalMetricsStatus(props: Record<string, unknown>) { + const { host, active } = props as unknown as TerminalToolbarStatusProps; + const { t } = useTranslation(); + const api = usePluginApi(); + const [metrics, setMetrics] = useState<Sample | null>(null); + const [available, setAvailable] = useState(true); + const hostId = host?.id ? Number(host.id) : null; + + useEffect(() => { + if (!active || !hostId) { + setMetrics(null); + return; + } + let cancelled = false; + let stopPolling: (() => void) | undefined; + let viewerSessionId: string | undefined; + let previous: Sample | null = null; + setAvailable(true); + + const poll = async () => { + try { + const response = await api.get<{ + cpu?: { percent?: number }; + memory?: { percent?: number }; + disk?: { percent?: number }; + } | null>(`/metrics/${hostId}`, { + validateStatus: (status: number) => status === 200 || status === 404, + }); + const data = response.data; + if (cancelled || !data) return; + const next: Sample = { + cpu: data.cpu?.percent ?? null, + memory: data.memory?.percent ?? null, + disk: data.disk?.percent ?? null, + }; + const changed = + !previous || + (["cpu", "memory", "disk"] as const).some((key) => { + const before = previous?.[key]; + const now = next[key]; + return before == null || now == null || Math.abs(now - before) >= 2; + }); + previous = next; + setMetrics(next); + return changed; + } catch { + // Keep the previous sample after a transient polling error. + return false; + } + }; + + const start = async () => { + try { + const { data } = await api.post<{ + requires_totp?: boolean; + viewerSessionId?: string; + }>(`/metrics/start/${hostId}`); + if (cancelled) { + if (data.viewerSessionId) { + void api + .post(`/metrics/stop/${hostId}`, { + viewerSessionId: data.viewerSessionId, + }) + .catch(() => {}); + } + return; + } + if (data.requires_totp) { + setAvailable(false); + return; + } + viewerSessionId = data.viewerSessionId; + stopPolling = runAdaptivePolling( + poll, + { + minIntervalMs: 5_000, + maxIntervalMs: 20_000, + stablePollsPerStep: 2, + maxRequestDutyCycle: 0.2, + }, + { intervalMultiplier: getPollingEnvironmentMultiplier }, + ); + } catch { + if (!cancelled) setAvailable(false); + } + }; + void start(); + + return () => { + cancelled = true; + stopPolling?.(); + void api + .post(`/metrics/stop/${hostId}`, { viewerSessionId }) + .catch(() => {}); + }; + }, [active, hostId, api]); + + if (!available) { + return ( + <span role="status" className="px-2 py-1 text-xs text-muted-foreground"> + {t("terminalStatus.unavailable")} + </span> + ); + } + + return ( + <> + <StatBar label={t("hostMetrics.cpu")} percent={metrics?.cpu ?? null} /> + <StatBar + label={t("hostMetrics.memory")} + percent={metrics?.memory ?? null} + /> + <StatBar label={t("hostMetrics.disk")} percent={metrics?.disk ?? null} /> + </> + ); +} diff --git a/plugins/host-metrics/src/frontend/index.tsx b/plugins/host-metrics/src/frontend/index.tsx index 4055d3c05..4a3728237 100644 --- a/plugins/host-metrics/src/frontend/index.tsx +++ b/plugins/host-metrics/src/frontend/index.tsx @@ -13,6 +13,7 @@ import HostMetricsApp from "./HostMetricsApp"; import { HostStatsTab } from "./HostEditorStatsTab"; import { metricsChartWidget } from "./MetricsChartWidget"; import { MetricsRetentionSetting } from "./MetricsRetentionSetting"; +import { TerminalMetricsStatus } from "./TerminalMetricsStatus"; type SectionSetField = Parameters<typeof HostStatsTab>[0]["setField"]; @@ -90,6 +91,17 @@ export function activate(app: TermixApp): void { app.registerSettingsComponent("retention", MetricsRetentionSetting); + // Live CPU, memory and disk bars in the terminal toolbar's expanded view. + app.registerSlotContribution("terminal.toolbarStatus", { + actionId: "host-metrics.terminalStatus", + titleKey: "nav.hostMetrics", + kind: "component", + component: TerminalMetricsStatus, + when: (context) => + (context.host as { statsConfig?: { metricsEnabled?: boolean } }) + ?.statsConfig?.metricsEnabled !== false, + }); + app.registerSlotContribution("onboarding.features", { actionId: "host-metrics.feature", titleKey: "onboarding.feature_metrics", diff --git a/plugins/host-metrics/tests/frontend/TerminalMetricsStatus.test.tsx b/plugins/host-metrics/tests/frontend/TerminalMetricsStatus.test.tsx new file mode 100644 index 000000000..866004460 --- /dev/null +++ b/plugins/host-metrics/tests/frontend/TerminalMetricsStatus.test.tsx @@ -0,0 +1,80 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { cleanup, render, screen, waitFor } from "@testing-library/react"; +import "@testing-library/jest-dom/vitest"; + +const api = vi.hoisted(() => ({ + get: vi.fn(), + post: vi.fn(), +})); + +vi.mock("@termix/plugin-sdk/frontend", async (importOriginal) => ({ + ...(await importOriginal<Record<string, unknown>>()), + usePluginApi: () => api, + useTranslation: () => ({ t: (key: string) => key, language: "en" }), +})); + +vi.mock("@termix/plugin-sdk/ui", () => ({ + cn: (...classes: unknown[]) => classes.filter(Boolean).join(" "), + getPollingEnvironmentMultiplier: () => 1, + runAdaptivePolling: (poll: () => Promise<unknown>) => { + void poll(); + return () => {}; + }, +})); + +import { TerminalMetricsStatus } from "../../src/frontend/TerminalMetricsStatus"; + +afterEach(() => { + cleanup(); + vi.clearAllMocks(); +}); + +describe("TerminalMetricsStatus", () => { + it("polls the host's metrics while active and shows the bars", async () => { + api.post.mockResolvedValue({ data: { viewerSessionId: "viewer-1" } }); + api.get.mockResolvedValue({ + data: { + cpu: { percent: 12 }, + memory: { percent: 40 }, + disk: { percent: 91 }, + }, + }); + + render(<TerminalMetricsStatus host={{ id: 7 }} isConnected active />); + + await waitFor(() => expect(screen.getByText("91%")).toBeInTheDocument()); + expect(api.post).toHaveBeenCalledWith("/metrics/start/7"); + expect(screen.getByText("12%")).toBeInTheDocument(); + expect(screen.getByText("hostMetrics.cpu")).toBeInTheDocument(); + }); + + it("does not poll while inactive", () => { + render( + <TerminalMetricsStatus host={{ id: 7 }} isConnected active={false} />, + ); + expect(api.post).not.toHaveBeenCalled(); + }); + + it("says stats are unavailable when the host needs a second factor", async () => { + api.post.mockResolvedValue({ data: { requires_totp: true } }); + render(<TerminalMetricsStatus host={{ id: 7 }} isConnected active />); + await waitFor(() => + expect( + screen.getByText("terminalStatus.unavailable"), + ).toBeInTheDocument(), + ); + }); + + it("stops its viewer on unmount", async () => { + api.post.mockResolvedValue({ data: { viewerSessionId: "viewer-2" } }); + api.get.mockResolvedValue({ data: null }); + const view = render( + <TerminalMetricsStatus host={{ id: 7 }} isConnected active />, + ); + await waitFor(() => expect(api.get).toHaveBeenCalled()); + view.unmount(); + expect(api.post).toHaveBeenLastCalledWith("/metrics/stop/7", { + viewerSessionId: "viewer-2", + }); + }); +}); diff --git a/plugins/ssh-terminal/CHANGELOG.md b/plugins/ssh-terminal/CHANGELOG.md index 6a1bc3a37..c1fd28d04 100644 --- a/plugins/ssh-terminal/CHANGELOG.md +++ b/plugins/ssh-terminal/CHANGELOG.md @@ -2,4 +2,6 @@ ## 1.0.0 -Bundled with Termix 2.9.0. +Bundled with Termix 2.9.0. The terminal, its session manager, the desktop +app's local terminal and command history live in this plugin now, with their +settings under the plugin's own admin and host settings. diff --git a/plugins/ssh-terminal/README.md b/plugins/ssh-terminal/README.md deleted file mode 100644 index 35bff76d9..000000000 --- a/plugins/ssh-terminal/README.md +++ /dev/null @@ -1,70 +0,0 @@ -# SSH Terminal - -The interactive SSH terminal, packaged as a first-party plugin. - -## Why this plugin runs in-process - -Every other plugin runs in a `worker_threads` worker and reaches the server -through a structured-clone `postMessage` boundary. That boundary is what makes -the capability gate and the audit trail mean anything: a worker plugin cannot -hold a db handle, an ssh2 `Client` or a live socket, so every privileged action -has to go through the broker. - -This plugin cannot work that way. It _is_ the SSH transport rather than a -consumer of one: - -- it owns a `WebSocketServer` on port 30002, -- it holds long-lived `ssh2.Client` objects and PTY streams, -- it needs plaintext host credentials to connect at all. - -None of those can cross a structured-clone boundary, and `ctx.hosts` returns a -13-field allowlist that deliberately excludes `password`, `key` and -`keyPassword`. So `ssh-terminal` runs on the main thread instead, under the -`process:transport-owner` tier. - -Membership in that tier is a hardcoded allowlist in -`src/backend/plugins/first-party.ts`. A manifest cannot ask for it: declaring -`process:transport-owner` without being on the list is a validation error. The -tier has no capability gate and no audit interception, which is only defensible -because nothing installable can enter it. - -## What lives where - -The terminal's implementation stays in `src/backend/hosts/terminal/`. Only the -lifecycle lives here. Two reasons it was not copied in: - -- `session-manager.ts` has six consumers outside the terminal (terminal routes, - open-tabs, user-image-storage, collab, and session-sharing twice). It is - shared infrastructure, not plugin-private code. -- host key verification (`host-key-verifier.ts`), jump host chaining - (`jump-host-chain.ts`) and host resolution (`host-resolver.ts`) each have - around eight consumers already. They keep working by _not_ moving. - -The SSH connection pool also stays in core. The terminal never used it: it -calls `new Client()` directly. The pool's consumers are metrics, tmux, tunnel, -fleet routes, AI tools, automations and `ctx.ssh` itself, so moving it here -would have made all of them depend on the terminal being enabled. - -Only `SSHAuthManager` is terminal-exclusive. - -## WebSocket: still port 30002, not /plugin-api/ - -The terminal WS was deliberately left on its own port and path. - -`/plugin-api` has no auth middleware, so routing terminal auth through it would -be a regression. Beyond that, `/ssh/websocket/` is mapped in both nginx configs, -three frontend call sites build that URL (`Terminal.tsx`, `SharedSessionView`, -`CollabRoomTab`), Electron's `buildOriginWsUrl` pins `localPort: 30002`, and the -8-attempt reconnect plus detach/reattach buffering all depend on the current -path. - -The plugin owns the port's lifecycle instead: enabling it calls -`startTerminalServer()`, disabling it calls `stopTerminalServer()`, which closes -live sessions and frees the port. - -## Disabling - -Disabling behaves like any other plugin. No confirmation dialog, no special -casing. The terminal tab stops being offered from the host list, command -palette and quick connect, the port stops listening, and every other feature -(metrics, docker, tunnels, file manager) keeps working off its own connections. diff --git a/plugins/ssh-terminal/locales/en.json b/plugins/ssh-terminal/locales/en.json index 0967ef424..f8dd05e08 100644 --- a/plugins/ssh-terminal/locales/en.json +++ b/plugins/ssh-terminal/locales/en.json @@ -1 +1,226 @@ -{} +{ + "mobileKeyboard": { + "addQuickKey": "Add", + "arrowDown": "Arrow Down", + "arrowLeft": "Arrow Left", + "arrowRight": "Arrow Right", + "arrowUp": "Arrow Up", + "backTab": "⇥", + "ctrl": "Ctrl", + "delete": "Del", + "done": "Done", + "editQuickKeys": "Edit quick keys", + "end": "End", + "esc": "Esc", + "home": "Home", + "pageDown": "PgDn", + "pageUp": "PgUp", + "paste": "Paste", + "quickKeyPlaceholder": "e.g. sudo ", + "quickKeysDesc": "Tap × to remove. Supports up to 8 characters.", + "quickKeysTitle": "Quick Keys", + "removeQuickKey": "Remove", + "resetDefaults": "Reset to defaults", + "shift": "Shift", + "tab": "Tab" + }, + "terminal": { + "autoTmuxEnableFailed": "Failed to enable Auto-Tmux", + "autoTmuxEnabled": "Auto-Tmux enabled for {{host}}. It applies from the next connection.", + "clipboardHttpWarning": "Couldn't read the clipboard. Some browsers restrict clipboard access outside HTTPS - try Ctrl+V instead, or serve Termix over HTTPS.", + "closeTab": "Close", + "connecting": "Connecting...", + "connectionLost": "Connection lost", + "connectionRejected": "Connection rejected by server. Please check your authentication and network configuration.", + "connectionTimeout": "Connection timeout", + "enableAutoTmuxAction": "Enable Auto-Tmux", + "hostKeyRejected": "SSH host key verification rejected. Connection cancelled.", + "linkDialogCopy": "Copy", + "linkDialogOpen": "Open", + "linkDialogTitle": "Open Link", + "maxReconnectAttemptsReached": "Maximum reconnection attempts reached", + "noHostSelected": "No host selected", + "opksshAuthFailed": "Authentication failed. Please check your credentials and try again.", + "opksshTimeout": "Authentication timed out. Please try again.", + "passwordPromptFillTitle": "Fill the saved password into this prompt?", + "reconnect": "Reconnect", + "reconnected": "Reconnected successfully", + "reconnecting": "Reconnecting... ({{attempt}}/{{max}})", + "searchCaseSensitive": "Match Case", + "searchClose": "Close (Escape)", + "searchNext": "Next Match (Enter)", + "searchNoResults": "No results", + "searchPlaceholder": "Find", + "searchPrevious": "Previous Match (Shift+Enter)", + "searchRegex": "Use Regular Expression", + "searchResultCount": "{{index}} of {{count}}", + "searchWholeWord": "Match Whole Word", + "sessionExpiredNotice": "Your previous session on {{host}} expired after {{minutes}} minutes disconnected; the shell and anything running in it are gone. Enable Auto-Tmux on this host to keep sessions alive.", + "sessionExpiredNoticeNoMinutes": "Your previous session on {{host}} expired while disconnected; the shell and anything running in it are gone. Enable Auto-Tmux on this host to keep sessions alive.", + "sessionTakenOver": "Session was opened in another tab. Reconnecting...", + "tmuxAttached": "Attached clients", + "tmuxAttachedCount": "{{count}} attached", + "tmuxCopyHint": "Adjust selection and press Enter to copy to clipboard", + "tmuxCreateNew": "Start new session", + "tmuxDetached": "Detached from tmux session", + "tmuxLastActivity": "Last activity", + "tmuxSessionAttached": "tmux session attached: {{name}}", + "tmuxSessionCreated": "tmux session created: {{name}}", + "tmuxSessionPickerDesc": "Existing tmux sessions found on this host. Select one to reattach or create a new session.", + "tmuxSessionPickerTitle": "tmux Sessions", + "tmuxTimeDays": "{{count}}d ago", + "tmuxTimeHours": "{{count}}h ago", + "tmuxTimeJustNow": "just now", + "tmuxTimeMinutes": "{{count}}m ago", + "tmuxUnavailable": "tmux is not installed on the remote host, falling back to standard shell", + "tmuxWindowCount": "{{count}} window", + "tmuxWindows": "Windows", + "unknownError": "Unknown error occurred", + "vaultAuthDescription": "A window has opened to sign in to HashiCorp Vault. Complete the sign-in there; this connection will continue automatically.", + "vaultAuthFailed": "Vault authentication failed. Please try again.", + "vaultAuthTitle": "Vault sign-in required", + "vaultReopen": "Reopen sign-in window", + "websocketAbnormalClose": "Connection closed unexpectedly. This may be due to a reverse proxy or SSL configuration issue. Please check server logs.", + "websocketError": "WebSocket connection error" + }, + "terminalToolbar": { + "detachTmux": "Detach tmux", + "detachTmuxDescription": "Detach tmux session", + "dismissError": "Dismiss image error", + "hideToolbar": "Hide toolbar", + "image": "Image actions (images only)", + "imageActionFailed": "Image action failed", + "layout": "Toolbar display mode", + "layoutExpanded": "Expanded with metrics", + "layoutIcon": "Icon only", + "layoutLabeled": "Icons and labels", + "moveToolbar": "Move toolbar", + "moveToolbarHint": "Drag to reposition the toolbar", + "paste": "Paste image", + "pasteImage": "Paste image from clipboard (images only)", + "retry": "Retry", + "showToolbar": "Expand toolbar", + "upload": "Upload image", + "uploadImage": "Upload an image to the host", + "uploadingImage": "Uploading..." + }, + "history": { + "noTerminalSelected": "No terminal selected", + "noTerminalSelectedHint": "Open an SSH terminal tab to view its command history", + "searchPlaceholder": "Search history...", + "clearAll": "Clear All", + "noHistoryEntries": "No history entries", + "trackingDisabled": "History tracking is disabled", + "trackingDisabledHint": "Turn on Command History for this host in its SSH Terminal plugin settings.", + "commandCount_one": "{{count}} command", + "commandCount_other": "{{count}} commands" + }, + "touchInput": { + "enabled": "Enable Touch Input", + "enabledDesc": "Allow one-finger touch scrolling in terminals", + "momentumEnabled": "Enable Momentum", + "momentumEnabledDesc": "Continue scrolling briefly after a swipe", + "advanced": "Advanced", + "resetDefaults": "Reset to defaults", + "dragThreshold": "Drag threshold", + "maxWheelDelta": "Maximum wheel delta", + "momentumSampleWindow": "Momentum sample window", + "releaseGracePeriod": "Release grace period", + "minimumVelocity": "Minimum velocity", + "maximumVelocity": "Maximum velocity", + "maximumDuration": "Maximum duration", + "maximumTravel": "Maximum travel", + "decayTime": "Decay time", + "pixelsPerTick": "Pixels per tick", + "maximumFrameInterval": "Maximum frame interval", + "maximumTicksPerFrame": "Maximum ticks per frame" + }, + "imageStorage": { + "instanceId": "Terminal session instance ID", + "instanceIdDesc": "Instance ID of one of your connected terminal tabs, used to test visibility.", + "selectedModeUnavailable": "Unavailable", + "test": "Test", + "testing": "Testing...", + "testFailed": "Failed to test image storage", + "testConnected": "Terminal session is connected.", + "testNotConnected": "No connected terminal session with this instance ID.", + "testSelectedMode": "An upload would use: {{mode}}", + "testLocalVisible": "The local directory is visible from the connected host.", + "testLocalNotVisible": "The local directory is NOT visible from the connected host." + }, + "settings": { + "admin": { + "group": { + "touch": "Touch Input", + "images": "Image Storage", + "sessions": "Sessions" + }, + "sessionTimeoutMinutes": { + "label": "Terminal Session Persistence", + "description": "How long a disconnected terminal is kept alive on the server before it is closed. Min 1 · Max 1440 minutes. For sessions that must outlive this, enable Auto-Tmux on the host." + }, + "commandHistoryEnabled": { + "label": "Command History", + "description": "Allow command history recording. When disabled, history is not saved regardless of per-host settings." + }, + "imageStorageMode": { + "label": "Storage Mode", + "description": "Where pasted and uploaded terminal images are written. Auto picks by connection, Local and Remote (SFTP) are fixed.", + "auto": "Auto", + "local": "Local", + "remoteSftp": "Remote (SFTP)" + }, + "imageLocalDir": { + "label": "Local directory", + "description": "Absolute path on the Termix backend where local mode writes images. Never shown after saving. Empty uses TERMIX_IMAGE_DIR." + }, + "imageHostPath": { + "label": "Host path", + "description": "The host-side view of the local directory that terminal agents receive." + }, + "imageTtlMs": { + "label": "TTL (ms)", + "description": "How long an uploaded image is kept, in milliseconds. Empty uses TERMIX_IMAGE_TTL_MS or one hour." + }, + "imageMaxCount": { + "label": "Max image count", + "description": "Most images kept at once. Empty uses TERMIX_MAX_IMAGE_COUNT or 100." + }, + "imageMaxBytes": { + "label": "Max storage bytes", + "description": "Most bytes of images kept at once. Empty uses TERMIX_MAX_IMAGE_STORAGE_BYTES or 5 GiB." + }, + "sessionPersistence": { + "label": "Keep sessions after disconnect", + "description": "Keep a disconnected terminal alive on the server for the time above, so reopening its tab picks up where it left off." + } + }, + "host": { + "enableTerminalToolbar": { + "label": "Enable Terminal Toolbar", + "description": "Show a quick-access toolbar docked to the active terminal for this host's tools and live stats" + }, + "enableCommandHistory": { + "label": "Command History", + "description": "Record commands run in this terminal for history and autocomplete" + }, + "enableTerminal": { + "label": "Enable Terminal", + "description": "Offer an SSH terminal for this host." + } + } + }, + "palette": { + "localTerminal": "Local Terminal" + }, + "permissions": { + "sessions": { + "title": "Use live terminal sessions", + "description": "Lets other features, such as session sharing and recording, reach your live terminal sessions." + }, + "history": { + "title": "Read command history", + "description": "Lets other features, such as the AI assistant, read your command history." + } + } +} diff --git a/plugins/ssh-terminal/locales/translated/af_ZA.json b/plugins/ssh-terminal/locales/translated/af_ZA.json new file mode 100644 index 000000000..e1303c2af --- /dev/null +++ b/plugins/ssh-terminal/locales/translated/af_ZA.json @@ -0,0 +1,203 @@ +{ + "mobileKeyboard": { + "addQuickKey": "Voeg by", + "arrowDown": "Pyltjie Af", + "arrowLeft": "Pyltjie links", + "arrowRight": "Pyltjie Regs", + "arrowUp": "Pyltjie Op", + "backTab": "⇥", + "ctrl": "Ctrl", + "delete": "Del", + "done": "Klaar", + "editQuickKeys": "Wysig vinnige sleutels", + "end": "Einde", + "esc": "Esc", + "home": "Tuis", + "pageDown": "PgDn", + "pageUp": "Bladsy Op", + "paste": "Plak", + "quickKeyPlaceholder": "bv. sudo ", + "quickKeysDesc": "Tik × om te verwyder. Ondersteun tot 8 karakters.", + "quickKeysTitle": "Vinnige sleutels", + "removeQuickKey": "Verwyder", + "resetDefaults": "Stel terug na verstekwaardes", + "shift": "Skof", + "tab": "Oortjie" + }, + "terminal": { + "autoTmuxEnableFailed": "Kon nie Auto-Tmux aktiveer nie", + "autoTmuxEnabled": "Auto-Tmux geaktiveer vir {{host}}. Dit geld vanaf die volgende verbinding.", + "clipboardHttpWarning": "Kon nie die knipbord lees nie. Sommige blaaiers beperk toegang tot die knipbord buite HTTPS - probeer eerder Ctrl+V, of bedien Termix oor HTTPS.", + "closeTab": "Maak toe", + "connecting": "Verbind...", + "connectionLost": "Verbinding verloor", + "connectionRejected": "Verbinding deur bediener verwerp. Kontroleer asseblief u verifikasie en netwerkkonfigurasie.", + "connectionTimeout": "Verbindingstydverstryking", + "enableAutoTmuxAction": "Aktiveer Auto-Tmux", + "hostKeyRejected": "SSH-gasheersleutelverifikasie verwerp. Verbinding gekanselleer.", + "linkDialogCopy": "Kopieer", + "linkDialogOpen": "Oop", + "linkDialogTitle": "Maak skakel oop", + "maxReconnectAttemptsReached": "Maksimum herverbindingspogings bereik", + "noHostSelected": "Geen gasheer gekies nie", + "opksshAuthFailed": "Verifikasie het misluk. Kontroleer asseblief jou geloofsbriewe en probeer weer.", + "opksshTimeout": "Verifikasie het verstryk. Probeer asseblief weer.", + "passwordPromptFillTitle": "Vul die gestoorde wagwoord in hierdie aanwysingsprompt in?", + "reconnect": "Herkoppel", + "reconnected": "Herverbind suksesvol", + "reconnecting": "Herverbind... ({{attempt}}/{{max}})", + "searchCaseSensitive": "Pasmaat", + "searchClose": "Maak toe (Escape)", + "searchNext": "Volgende Wedstryd (Enter)", + "searchNoResults": "Geen resultate nie", + "searchPlaceholder": "Vind", + "searchPrevious": "Vorige Wedersydse (Shift+Enter)", + "searchRegex": "Gebruik Gereelde Uitdrukking", + "searchResultCount": "{{index}} van {{count}}", + "searchWholeWord": "Pas die hele woord by", + "sessionExpiredNotice": "Jou vorige sessie op {{host}} het verval nadat {{minutes}} minute ontkoppel was; die dop en enigiets wat daarin loop, is weg. Aktiveer Auto-Tmux op hierdie gasheer om sessies aan die gang te hou.", + "sessionExpiredNoticeNoMinutes": "Jou vorige sessie op {{host}} het verval terwyl dit ontkoppel was; die dop en enigiets wat daarin loop, is weg. Aktiveer Auto-Tmux op hierdie gasheer om sessies aan die gang te hou.", + "sessionTakenOver": "Sessie is in 'n ander oortjie oopgemaak. Herverbind tans...", + "tmuxAttached": "Aangehegte kliënte", + "tmuxAttachedCount": "{{count}} aangeheg", + "tmuxCopyHint": "Pas seleksie aan en druk Enter om na knipbord te kopieer", + "tmuxCreateNew": "Begin nuwe sessie", + "tmuxDetached": "Losgemaak van tmux-sessie", + "tmuxLastActivity": "Laaste aktiwiteit", + "tmuxSessionAttached": "tmux-sessie aangeheg: {{name}}", + "tmuxSessionCreated": "tmux-sessie geskep: {{name}}", + "tmuxSessionPickerDesc": "Bestaande tmux-sessies is op hierdie gasheer gevind. Kies een om weer aan te heg of skep 'n nuwe sessie.", + "tmuxSessionPickerTitle": "tmux-sessies", + "tmuxTimeDays": "{{count}}dae gelede", + "tmuxTimeHours": "{{count}}uur gelede", + "tmuxTimeJustNow": "nou net", + "tmuxTimeMinutes": "{{count}}m gelede", + "tmuxUnavailable": "tmux is nie op die afgeleë gasheer geïnstalleer nie, en val terug na die standaard dop.", + "tmuxWindowCount": "{{count}} venster", + "tmuxWindows": "Vensters", + "unknownError": "Onbekende fout het voorgekom", + "vaultAuthDescription": "'n Venster het oopgemaak om by HashiCorp Vault aan te meld. Voltooi die aanmelding daar; hierdie verbinding sal outomaties voortgaan.", + "vaultAuthFailed": "Kluisverifikasie het misluk. Probeer asseblief weer.", + "vaultAuthTitle": "Kluisaanmelding vereis", + "vaultReopen": "Maak aanmeldvenster weer oop", + "websocketAbnormalClose": "Verbinding het onverwags gesluit. Dit kan wees as gevolg van 'n omgekeerde proxy- of SSL-konfigurasieprobleem. Gaan asseblief bedienerlogboeke na.", + "websocketError": "WebSocket-verbindingsfout" + }, + "terminalToolbar": { + "detachTmux": "Ontkoppel tmux", + "detachTmuxDescription": "Ontkoppel tmux-sessie", + "dismissError": "Maak beeldfout toe", + "hideToolbar": "Versteek nutsbalk", + "image": "Beeldaksies (slegs beelde)", + "imageActionFailed": "Beeldaksie het misluk", + "layout": "Nutsbalk-vertoonmodus", + "layoutExpanded": "Uitgebrei met statistieke", + "layoutIcon": "Slegs ikoon", + "layoutLabeled": "Ikone en etikette", + "moveToolbar": "Skuif nutsbalk", + "moveToolbarHint": "Sleep om die nutsbalk te herposisioneer", + "paste": "Plak beeld", + "pasteImage": "Plak beeld vanaf knipbord (slegs beelde)", + "retry": "Probeer weer", + "showToolbar": "Vou die nutsbalk uit", + "upload": "Laai beeld op", + "uploadImage": "Laai 'n beeld op na die gasheer", + "uploadingImage": "Laai tans op..." + }, + "history": { + "noTerminalSelected": "Geen terminaal gekies nie", + "noTerminalSelectedHint": "Maak 'n SSH-terminaal-oortjie oop om die opdraggeskiedenis te sien", + "searchPlaceholder": "Soekgeskiedenis...", + "clearAll": "Vee alles uit", + "noHistoryEntries": "Geen geskiedenisinskrywings nie", + "trackingDisabled": "Geskiedenisopsporing is gedeaktiveer" + }, + "touchInput": { + "enabled": "Aktiveer Aanraakinvoer", + "enabledDesc": "Laat eenvinger-aanraakblaai in terminale toe", + "momentumEnabled": "Aktiveer Momentum", + "momentumEnabledDesc": "Gaan voort met blaai kortliks na 'n swipe", + "advanced": "Gevorderd", + "resetDefaults": "Stel terug na verstekwaardes", + "dragThreshold": "Sleepdrempel", + "maxWheelDelta": "Maksimum wieldelta", + "momentumSampleWindow": "Momentum voorbeeldvenster", + "releaseGracePeriod": "Vrystellingsgrasietydperk", + "minimumVelocity": "Minimum snelheid", + "maximumVelocity": "Maksimum snelheid", + "maximumDuration": "Maksimum duur", + "maximumTravel": "Maksimum reis", + "decayTime": "Vervaltyd", + "pixelsPerTick": "Piksels per tik", + "maximumFrameInterval": "Maksimum raaminterval", + "maximumTicksPerFrame": "Maksimum aantal tikkies per raam" + }, + "imageStorage": { + "instanceId": "Terminale sessie-instansie-ID", + "instanceIdDesc": "Instansie-ID van een van jou gekoppelde terminaal-oortjies, wat gebruik word om sigbaarheid te toets.", + "selectedModeUnavailable": "Nie beskikbaar nie", + "test": "Toets", + "testing": "Toetsing...", + "testFailed": "Kon nie beeldberging toets nie", + "testConnected": "Terminaalsessie is gekoppel.", + "testNotConnected": "Geen gekoppelde terminaalsessie met hierdie instansie-ID nie.", + "testSelectedMode": "'n Oplaai sal gebruik: {{mode}}", + "testLocalVisible": "Die plaaslike gids is sigbaar vanaf die gekoppelde gasheer.", + "testLocalNotVisible": "Die plaaslike gids is NIE sigbaar vanaf die gekoppelde gasheer nie." + }, + "settings": { + "admin": { + "group": { + "touch": "Aanraakinvoer", + "images": "Beeldberging" + }, + "sessionTimeoutMinutes": { + "label": "Terminale Sessie Persistensie", + "description": "Hoe lank 'n ontkoppelde terminaal op die bediener aan die gang gehou word voordat dit gesluit word. Min 1 · Maks 1440 minute. Vir sessies wat dit moet oorleef, aktiveer Auto-Tmux op die gasheer." + }, + "commandHistoryEnabled": { + "label": "Opdraggeskiedenis", + "description": "Laat die opname van opdraggeskiedenis toe. Wanneer dit gedeaktiveer is, word geskiedenis nie gestoor nie, ongeag die instellings per gasheer." + }, + "imageStorageMode": { + "label": "Bergingsmodus", + "description": "Waar geplakte en opgelaaide terminaalbeelde geskryf word. Outomatiese seleksies per verbinding, Plaaslik en Afgeleë (SFTP) is reggestel.", + "auto": "Outomaties", + "local": "Plaaslik", + "remoteSftp": "Afstandsbediening (SFTP)" + }, + "imageLocalDir": { + "label": "Plaaslike gids" + }, + "imageHostPath": { + "label": "Gasheerpad", + "description": "Die gasheer-aansig van die plaaslike gids wat terminaalagente ontvang." + }, + "imageTtlMs": { + "label": "TTL (ms)" + }, + "imageMaxCount": { + "label": "Maksimum beeldtelling" + }, + "imageMaxBytes": { + "label": "Maksimum stoorplekgrepe" + } + }, + "host": { + "enableTerminalToolbar": { + "label": "Aktiveer Terminale-nutsbalk", + "description": "Wys 'n vinnige toegangsbalk wat aan die aktiewe terminaal gekoppel is vir hierdie gasheer se gereedskap en regstreekse statistieke." + }, + "enableCommandHistory": { + "label": "Opdraggeskiedenis", + "description": "Rekordopdragte word in hierdie terminaal uitgevoer vir geskiedenis en outovoltooiing" + }, + "enableTerminal": { + "label": "Aktiveer Terminaal" + } + } + }, + "palette": { + "localTerminal": "Plaaslike Terminaal" + } +} diff --git a/plugins/ssh-terminal/locales/translated/ar_SA.json b/plugins/ssh-terminal/locales/translated/ar_SA.json new file mode 100644 index 000000000..c5f9ce602 --- /dev/null +++ b/plugins/ssh-terminal/locales/translated/ar_SA.json @@ -0,0 +1,203 @@ +{ + "mobileKeyboard": { + "addQuickKey": "إضافة", + "arrowDown": "Arrow Down", + "arrowLeft": "Arrow Left", + "arrowRight": "Arrow Right", + "arrowUp": "Arrow Up", + "backTab": "⇥", + "ctrl": "Ctrl", + "delete": "Del", + "done": "تم", + "editQuickKeys": "تحرير المفاتيح السريعة", + "end": "End", + "esc": "Esc", + "home": "Home", + "pageDown": "PgDn", + "pageUp": "PgUp", + "paste": "لصق", + "quickKeyPlaceholder": "مثال: sudo ", + "quickKeysDesc": "اضغط × للإزالة. يدعم حتى 8 أحرف.", + "quickKeysTitle": "المفاتيح السريعة", + "removeQuickKey": "إزالة", + "resetDefaults": "استعادة الإعدادات الافتراضية", + "shift": "Shift", + "tab": "Tab" + }, + "terminal": { + "autoTmuxEnableFailed": "فشل تفعيل خاصية Auto-Tmux", + "autoTmuxEnabled": "تم تفعيل خاصية Auto-Tmux لـ {{host}}. وسيتم تطبيقها بدءًا من الاتصال التالي.", + "clipboardHttpWarning": "تعذر قراءة الحافظة. بعض المتصفحات تقيّد الوصول إلى الحافظة خارج بروتوكول HTTPS - جرّب Ctrl+V بدلاً من ذلك، أو شغّل Termix عبر HTTPS.", + "closeTab": "إغلاق", + "connecting": "جارٍ الاتصال...", + "connectionLost": "انقطع الاتصال", + "connectionRejected": "تم رفض الاتصال من قبل الخادم. يرجى التحقق من المصادقة وإعدادات الشبكة.", + "connectionTimeout": "انتهت مهلة الاتصال", + "enableAutoTmuxAction": "تفعيل خاصية Auto-Tmux", + "hostKeyRejected": "تم رفض التحقق من مفتاح مضيف SSH. تم إلغاء الاتصال.", + "linkDialogCopy": "نسخ", + "linkDialogOpen": "فتح", + "linkDialogTitle": "فتح الرابط", + "maxReconnectAttemptsReached": "تم الوصول إلى الحد الأقصى لمحاولات إعادة الاتصال", + "noHostSelected": "لم يتم تحديد مضيف", + "opksshAuthFailed": "فشلت المصادقة. يرجى التحقق من بيانات الاعتماد والمحاولة مرة أخرى.", + "opksshTimeout": "انتهت مهلة المصادقة. يرجى المحاولة مرة أخرى.", + "passwordPromptFillTitle": "هل تريد ملء كلمة المرور المحفوظة في هذه النافذة؟", + "reconnect": "إعادة الاتصال", + "reconnected": "تمت إعادة الاتصال بنجاح", + "reconnecting": "جارٍ إعادة الاتصال... ({{attempt}}/{{max}})", + "searchCaseSensitive": "علبة كبريت", + "searchClose": "إغلاق (الهروب)", + "searchNext": "المباراة التالية (إدخال)", + "searchNoResults": "لا توجد نتائج", + "searchPlaceholder": "يجد", + "searchPrevious": "المباراة السابقة (Shift+Enter)", + "searchRegex": "استخدم التعبير النمطي", + "searchResultCount": "{{index}} من {{count}}", + "searchWholeWord": "تطابق الكلمة الكاملة", + "sessionExpiredNotice": "انتهت صلاحية جلستك السابقة على {{host}} بعد {{minutes}} دقيقة من قطع الاتصال؛ تم إغلاق نافذة الأوامر وأي شيء يعمل بداخلها. فعّل خاصية Auto-Tmux على هذا المضيف للحفاظ على استمرارية الجلسات.", + "sessionExpiredNoticeNoMinutes": "انتهت صلاحية جلستك السابقة على {{host}} أثناء انقطاع الاتصال؛ تم حذف واجهة سطر الأوامر وأي شيء كان يعمل بداخلها. فعّل خاصية Auto-Tmux على هذا المضيف للحفاظ على استمرارية الجلسات.", + "sessionTakenOver": "تم فتح الجلسة في علامة تبويب أخرى. جارٍ إعادة الاتصال...", + "tmuxAttached": "العملاء المتصلون", + "tmuxAttachedCount": "{{count}} متصل", + "tmuxCopyHint": "اضبط التحديد واضغط Enter للنسخ إلى الحافظة", + "tmuxCreateNew": "بدء جلسة جديدة", + "tmuxDetached": "تم الفصل من جلسة tmux", + "tmuxLastActivity": "آخر نشاط", + "tmuxSessionAttached": "تم الالتحاق بجلسة tmux: {{name}}", + "tmuxSessionCreated": "تم إنشاء جلسة tmux: {{name}}", + "tmuxSessionPickerDesc": "تم العثور على جلسات tmux موجودة على هذا المضيف. اختر واحدة لإعادة الاتصال أو أنشئ جلسة جديدة.", + "tmuxSessionPickerTitle": "جلسات tmux", + "tmuxTimeDays": "منذ {{count}}ي", + "tmuxTimeHours": "منذ {{count}}س", + "tmuxTimeJustNow": "الآن فقط", + "tmuxTimeMinutes": "منذ {{count}}د", + "tmuxUnavailable": "tmux غير مثبّت على المضيف البعيد، جارٍ الرجوع إلى الصدفة القياسية", + "tmuxWindowCount": "{{count}} نافذة", + "tmuxWindows": "النوافذ", + "unknownError": "حدث خطأ غير معروف", + "vaultAuthDescription": "تم فتح نافذة لتسجيل الدخول إلى HashiCorp Vault. أكمل تسجيل الدخول هناك؛ سيستمر هذا الاتصال تلقائيًا.", + "vaultAuthFailed": "فشلت مصادقة Vault. يرجى المحاولة مرة أخرى.", + "vaultAuthTitle": "مطلوب تسجيل الدخول إلى Vault", + "vaultReopen": "أعد فتح نافذة تسجيل الدخول", + "websocketAbnormalClose": "أُغلق الاتصال بشكل غير متوقع. قد يكون السبب مشكلة في الوكيل العكسي أو إعدادات SSL. يرجى مراجعة سجلات الخادم.", + "websocketError": "خطأ في اتصال WebSocket" + }, + "terminalToolbar": { + "detachTmux": "افصل tmux", + "detachTmuxDescription": "فصل جلسة tmux", + "dismissError": "تجاهل خطأ الصورة", + "hideToolbar": "إخفاء شريط الأدوات", + "image": "إجراءات الصور (للصور فقط)", + "imageActionFailed": "فشلت عملية معالجة الصورة", + "layout": "وضع عرض شريط الأدوات", + "layoutExpanded": "تم توسيعه بمقاييس", + "layoutIcon": "أيقونة فقط", + "layoutLabeled": "الرموز والملصقات", + "moveToolbar": "نقل شريط الأدوات", + "moveToolbarHint": "اسحب لإعادة وضع شريط الأدوات", + "paste": "لصق الصورة", + "pasteImage": "لصق الصورة من الحافظة (الصور فقط)", + "retry": "إعادة المحاولة", + "showToolbar": "توسيع شريط الأدوات", + "upload": "تحميل صورة", + "uploadImage": "قم بتحميل صورة إلى المضيف", + "uploadingImage": "جارٍ التحميل..." + }, + "history": { + "noTerminalSelected": "لا توجد طرفية محددة", + "noTerminalSelectedHint": "افتح علامة تبويب طرفية SSH لعرض سجل الأوامر الخاص بها", + "searchPlaceholder": "ابحث في السجل...", + "clearAll": "مسح الكل", + "noHistoryEntries": "لا توجد مدخلات سجل", + "trackingDisabled": "تتبع السجل معطل" + }, + "touchInput": { + "enabled": "تفعيل إدخال اللمس", + "enabledDesc": "السماح بالتمرير بلمسة إصبع واحدة في المحطات الطرفية", + "momentumEnabled": "تفعيل الزخم", + "momentumEnabledDesc": "استمر في التمرير لفترة وجيزة بعد التمرير", + "advanced": "متقدم", + "resetDefaults": "إعادة ضبط الإعدادات الافتراضية", + "dragThreshold": "عتبة السحب", + "maxWheelDelta": "أقصى دلتا للعجلة", + "momentumSampleWindow": "نافذة عينة الزخم", + "releaseGracePeriod": "فترة السماح للإفراج", + "minimumVelocity": "الحد الأدنى للسرعة", + "maximumVelocity": "أقصى سرعة", + "maximumDuration": "أقصى مدة", + "maximumTravel": "أقصى قدر من السفر", + "decayTime": "وقت التحلل", + "pixelsPerTick": "بكسل لكل نبضة", + "maximumFrameInterval": "أقصى فاصل زمني بين الإطارات", + "maximumTicksPerFrame": "الحد الأقصى للنبضات لكل إطار" + }, + "imageStorage": { + "instanceId": "معرّف مثيل جلسة الطرفية", + "instanceIdDesc": "معرف مثيل إحدى علامات تبويب الطرفية المتصلة، المستخدمة لاختبار الرؤية.", + "selectedModeUnavailable": "غير متاح", + "test": "امتحان", + "testing": "جارٍ الاختبار...", + "testFailed": "فشل اختبار تخزين الصور", + "testConnected": "تم الاتصال بجلسة الطرفية.", + "testNotConnected": "لا توجد جلسة طرفية متصلة بهذا المعرف.", + "testSelectedMode": "سيتم استخدام الأمر التالي للتحميل: {{mode}}", + "testLocalVisible": "الدليل المحلي مرئي من الجهاز المضيف المتصل.", + "testLocalNotVisible": "الدليل المحلي غير مرئي من الجهاز المضيف المتصل." + }, + "settings": { + "admin": { + "group": { + "touch": "إدخال اللمس", + "images": "تخزين الصور" + }, + "sessionTimeoutMinutes": { + "label": "استمرار جلسة الطرفية", + "description": "مدة بقاء طرفية غير متصلة على الخادم قبل إغلاقها. الحد الأدنى: دقيقة واحدة، الحد الأقصى: 1440 دقيقة. بالنسبة للجلسات التي يجب أن تستمر لفترة أطول من ذلك، فعّل خاصية Auto-Tmux على المضيف." + }, + "commandHistoryEnabled": { + "label": "سجل الأوامر", + "description": "السماح بتسجيل سجل الأوامر. عند التعطيل، لا يتم حفظ السجل بغض النظر عن إعدادات كل جهاز." + }, + "imageStorageMode": { + "label": "وضع التخزين", + "description": "يتم حفظ الصور الطرفية الملصقة والمرفوعة في هذا المكان. يتم تثبيت عمليات الاختيار التلقائي حسب الاتصال، سواء كان محليًا أو عن بُعد (SFTP).", + "auto": "آلي", + "local": "محلي", + "remoteSftp": "عن بعد (SFTP)" + }, + "imageLocalDir": { + "label": "الدليل المحلي" + }, + "imageHostPath": { + "label": "مسار المضيف", + "description": "عرض الدليل المحلي من جانب المضيف الذي تتلقاه وكلاء المحطة الطرفية." + }, + "imageTtlMs": { + "label": "مدة البقاء (بالمللي ثانية)" + }, + "imageMaxCount": { + "label": "الحد الأقصى لعدد الصور" + }, + "imageMaxBytes": { + "label": "الحد الأقصى لحجم التخزين بالبايت" + } + }, + "host": { + "enableTerminalToolbar": { + "label": "تفعيل شريط أدوات الطرفية", + "description": "اعرض شريط أدوات سريع الوصول مثبتًا على الطرفية النشطة لأدوات هذا المضيف وإحصائياته المباشرة." + }, + "enableCommandHistory": { + "label": "سجل الأوامر", + "description": "تسجيل الأوامر المنفذة في هذه الطرفية للسجل والإكمال التلقائي" + }, + "enableTerminal": { + "label": "تمكين الطرفية" + } + } + }, + "palette": { + "localTerminal": "المحطة المحلية" + } +} diff --git a/plugins/ssh-terminal/locales/translated/bg_BG.json b/plugins/ssh-terminal/locales/translated/bg_BG.json new file mode 100644 index 000000000..ff78a0f2d --- /dev/null +++ b/plugins/ssh-terminal/locales/translated/bg_BG.json @@ -0,0 +1,203 @@ +{ + "mobileKeyboard": { + "addQuickKey": "Добавяне", + "arrowDown": "Стрелка надолу", + "arrowLeft": "Стрелка наляво", + "arrowRight": "Стрелка надясно", + "arrowUp": "Стрелка нагоре", + "backTab": "⇥", + "ctrl": "Ctrl", + "delete": "Дел", + "done": "Готово", + "editQuickKeys": "Редактиране на бързи клавиши", + "end": "Край", + "esc": "Esc", + "home": "Дом", + "pageDown": "PgDn", + "pageUp": "PgUp", + "paste": "Паста", + "quickKeyPlaceholder": "например sudo ", + "quickKeysDesc": "Докоснете × за премахване. Поддържа до 8 знака.", + "quickKeysTitle": "Бързи клавиши", + "removeQuickKey": "Премахване", + "resetDefaults": "Възстановяване на фабричните настройки", + "shift": "Изместване", + "tab": "Раздел" + }, + "terminal": { + "autoTmuxEnableFailed": "Неуспешно активиране на Auto-Tmux", + "autoTmuxEnabled": "Автоматичното Tmux управление е активирано за {{host}}. Прилага се от следващата връзка.", + "clipboardHttpWarning": "Не можах да прочета клипборда. Някои браузъри ограничават достъпа до клипборда извън HTTPS - опитайте Ctrl+V вместо това или използвайте Termix през HTTPS.", + "closeTab": "Затвори", + "connecting": "Свързване...", + "connectionLost": "Връзката е прекъсната", + "connectionRejected": "Връзката е отхвърлена от сървъра. Моля, проверете удостоверяването и мрежовата си конфигурация.", + "connectionTimeout": "Време за изчакване на връзката", + "enableAutoTmuxAction": "Активиране на Auto-Tmux", + "hostKeyRejected": "Проверката на SSH ключа за хост е отхвърлена. Връзката е прекратена.", + "linkDialogCopy": "Копиране", + "linkDialogOpen": "Отворено", + "linkDialogTitle": "Отвори линка", + "maxReconnectAttemptsReached": "Достигнат е максималният брой опити за повторно свързване", + "noHostSelected": "Няма избран хост", + "opksshAuthFailed": "Удостоверяването не бе успешно. Моля, проверете идентификационните си данни и опитайте отново.", + "opksshTimeout": "Времето за изчакване на удостоверяването изтече. Моля, опитайте отново.", + "passwordPromptFillTitle": "Да се попълни ли запазената парола в този ред?", + "reconnect": "Свържете се отново", + "reconnected": "Успешно повторно свързване", + "reconnecting": "Повторно свързване... ({{attempt}}/{{max}})", + "searchCaseSensitive": "Съвпадение на регистъра", + "searchClose": "Затвори (Escape)", + "searchNext": "Следващ мач (Enter)", + "searchNoResults": "Няма резултати", + "searchPlaceholder": "Намерете", + "searchPrevious": "Предишно съвпадение (Shift+Enter)", + "searchRegex": "Използвайте регулярни изрази", + "searchResultCount": "{{index}} от {{count}}", + "searchWholeWord": "Съвпадение на цяла дума", + "sessionExpiredNotice": "Предишната ви сесия на {{host}} изтече след {{minutes}} минути прекъснато свързване; обвивката и всичко, което се изпълнява в нея, са изчезнали. Активирайте Auto-Tmux на този хост, за да поддържате сесиите активни.", + "sessionExpiredNoticeNoMinutes": "Предишната ви сесия на {{host}} изтече, докато бяхте прекъснати; обвивката и всичко, което се изпълняваше в нея, са изчезнали. Активирайте Auto-Tmux на този хост, за да поддържате сесиите активни.", + "sessionTakenOver": "Сесията беше отворена в друг раздел. Повторно свързване...", + "tmuxAttached": "Прикачени клиенти", + "tmuxAttachedCount": "{{count}} прикачен", + "tmuxCopyHint": "Променете селекцията и натиснете Enter, за да я копирате в клипборда", + "tmuxCreateNew": "Започнете нова сесия", + "tmuxDetached": "Отделен от tmux сесията", + "tmuxLastActivity": "Последна активност", + "tmuxSessionAttached": "Прикачена tmux сесия: {{name}}", + "tmuxSessionCreated": "tmux сесия, създадена: {{name}}", + "tmuxSessionPickerDesc": "На този хост са открити съществуващи tmux сесии. Изберете една, за да я свържете отново или да създадете нова сесия.", + "tmuxSessionPickerTitle": "tmux сесии", + "tmuxTimeDays": "преди {{count}}дни", + "tmuxTimeHours": "преди {{count}}ч", + "tmuxTimeJustNow": "точно сега", + "tmuxTimeMinutes": "преди {{count}}мин.", + "tmuxUnavailable": "tmux не е инсталиран на отдалечения хост, връща се към стандартната обвивка", + "tmuxWindowCount": "{{count}} прозорец", + "tmuxWindows": "Прозорци", + "unknownError": "Възникна неизвестна грешка", + "vaultAuthDescription": "Отвори се прозорец за влизане в HashiCorp Vault. Завършете влизането там; тази връзка ще продължи автоматично.", + "vaultAuthFailed": "Удостоверяването на трезора не бе успешно. Моля, опитайте отново.", + "vaultAuthTitle": "Изисква се влизане в трезора", + "vaultReopen": "Отваряне отново на прозореца за влизане", + "websocketAbnormalClose": "Връзката се прекъсна неочаквано. Това може да се дължи на проблем с обратен прокси или SSL конфигурация. Моля, проверете лог файловете на сървъра.", + "websocketError": "Грешка при свързване с WebSocket" + }, + "terminalToolbar": { + "detachTmux": "Отделяне на tmux", + "detachTmuxDescription": "Отделяне на tmux сесия", + "dismissError": "Грешка при отхвърляне на изображението", + "hideToolbar": "Скриване на лентата с инструменти", + "image": "Действия с изображения (само с изображения)", + "imageActionFailed": "Действието с изображението не беше успешно", + "layout": "Режим на показване на лентата с инструменти", + "layoutExpanded": "Разширено с показатели", + "layoutIcon": "Само икона", + "layoutLabeled": "Икони и етикети", + "moveToolbar": "Преместване на лентата с инструменти", + "moveToolbarHint": "Плъзнете, за да препозиционирате лентата с инструменти", + "paste": "Поставяне на изображение", + "pasteImage": "Поставяне на изображение от клипборда (само за изображения)", + "retry": "Опитай отново", + "showToolbar": "Разгъване на лентата с инструменти", + "upload": "Качване на изображение", + "uploadImage": "Качете изображение на хоста", + "uploadingImage": "Качване..." + }, + "history": { + "noTerminalSelected": "Няма избран терминал", + "noTerminalSelectedHint": "Отворете раздел на SSH терминал, за да видите историята на командите му", + "searchPlaceholder": "История на търсенията...", + "clearAll": "Изчисти всички", + "noHistoryEntries": "Няма записи в историята", + "trackingDisabled": "Проследяването на историята е деактивирано" + }, + "touchInput": { + "enabled": "Активиране на сензорно въвеждане", + "enabledDesc": "Разрешаване на превъртане с един пръст в терминали", + "momentumEnabled": "Активиране на Momentum", + "momentumEnabledDesc": "Продължете да превъртате за кратко след плъзване", + "advanced": "Разширено", + "resetDefaults": "Възстановяване на фабричните настройки", + "dragThreshold": "Праг на плъзгане", + "maxWheelDelta": "Максимална делта на колелото", + "momentumSampleWindow": "Прозорец за пример на импулса", + "releaseGracePeriod": "Гратисен период за освобождаване", + "minimumVelocity": "Минимална скорост", + "maximumVelocity": "Максимална скорост", + "maximumDuration": "Максимална продължителност", + "maximumTravel": "Максимален ход", + "decayTime": "Време на разпад", + "pixelsPerTick": "Пиксели на такт", + "maximumFrameInterval": "Максимален интервал на кадрите", + "maximumTicksPerFrame": "Максимален брой отметки на кадър" + }, + "imageStorage": { + "instanceId": "Идентификатор на екземпляр на терминална сесия", + "instanceIdDesc": "Идентификатор на екземпляр на един от свързаните ви терминални раздели, използван за тестване на видимостта.", + "selectedModeUnavailable": "Не е налично", + "test": "Тест", + "testing": "Тестване...", + "testFailed": "Тестването на съхранението на изображения не бе успешно", + "testConnected": "Терминалната сесия е свързана.", + "testNotConnected": "Няма свързана терминална сесия с този идентификатор на екземпляр.", + "testSelectedMode": "Качването ще използва: {{mode}}", + "testLocalVisible": "Локалната директория е видима от свързания хост.", + "testLocalNotVisible": "Локалната директория НЕ е видима от свързания хост." + }, + "settings": { + "admin": { + "group": { + "touch": "Сензорно въвеждане", + "images": "Съхранение на изображения" + }, + "sessionTimeoutMinutes": { + "label": "Устойчивост на терминална сесия", + "description": "Колко дълго един прекъснат терминал се поддържа активен на сървъра, преди да бъде затворен. Мин. 1 · Макс. 1440 минути. За сесии, които трябва да преживеят това, активирайте Auto-Tmux на хоста." + }, + "commandHistoryEnabled": { + "label": "История на командите", + "description": "Разрешаване на записване на историята на командите. Когато е деактивирано, историята не се запазва, независимо от настройките за всеки хост." + }, + "imageStorageMode": { + "label": "Режим на съхранение", + "description": "Където се записват поставените и качени изображения на терминала. Автоматичното избиране по връзка, локално и отдалечено (SFTP) е поправено.", + "auto": "Автоматично", + "local": "Местно", + "remoteSftp": "Отдалечено (SFTP)" + }, + "imageLocalDir": { + "label": "Локален указател" + }, + "imageHostPath": { + "label": "Път на хоста", + "description": "Изгледът от страна на хоста на локалната директория, която получават терминалните агенти." + }, + "imageTtlMs": { + "label": "TTL (мсек)" + }, + "imageMaxCount": { + "label": "Максимален брой изображения" + }, + "imageMaxBytes": { + "label": "Максимален брой байтове за съхранение" + } + }, + "host": { + "enableTerminalToolbar": { + "label": "Активиране на лентата с инструменти на терминала", + "description": "Показване на лента с инструменти за бърз достъп, закачена към активния терминал, за инструментите и статистиката в реално време на този хост" + }, + "enableCommandHistory": { + "label": "История на командите", + "description": "Запис на команди, изпълнявани в този терминал, за история и автоматично довършване" + }, + "enableTerminal": { + "label": "Активиране на терминала" + } + } + }, + "palette": { + "localTerminal": "Местен терминал" + } +} diff --git a/plugins/ssh-terminal/locales/translated/bn_BD.json b/plugins/ssh-terminal/locales/translated/bn_BD.json new file mode 100644 index 000000000..d5b687e2b --- /dev/null +++ b/plugins/ssh-terminal/locales/translated/bn_BD.json @@ -0,0 +1,203 @@ +{ + "mobileKeyboard": { + "addQuickKey": "যোগ করুন", + "arrowDown": "অ্যারো ডাউন", + "arrowLeft": "বাম তীর", + "arrowRight": "ডান তীরচিহ্ন", + "arrowUp": "তীর ঊর্ধ্বমুখী", + "backTab": "⇥", + "ctrl": "Ctrl", + "delete": "ডেল", + "done": "সম্পন্ন হয়েছে", + "editQuickKeys": "দ্রুত কী সম্পাদনা করুন", + "end": "শেষ", + "esc": "এস্ক", + "home": "হোম", + "pageDown": "পিজিডিএন", + "pageUp": "পিজিআপ", + "paste": "পেস্ট", + "quickKeyPlaceholder": "যেমন sudo ", + "quickKeysDesc": "মুছে ফেলতে × চাপুন। সর্বোচ্চ ৮টি অক্ষর সমর্থন করে।", + "quickKeysTitle": "দ্রুত চাবি", + "removeQuickKey": "অপসারণ করুন", + "resetDefaults": "ডিফল্টে রিসেট করুন", + "shift": "শিফট", + "tab": "ট্যাব" + }, + "terminal": { + "autoTmuxEnableFailed": "অটো-টিমাক্স সক্রিয় করতে ব্যর্থ হয়েছে", + "autoTmuxEnabled": "{{host}}এর জন্য অটো-টিমাক্স সক্রিয় করা হয়েছে। এটি পরবর্তী সংযোগ থেকে প্রযোজ্য হবে।", + "clipboardHttpWarning": "ক্লিপবোর্ডটি পড়া যায়নি। কিছু ব্রাউজার HTTPS-এর বাইরে ক্লিপবোর্ড অ্যাক্সেস সীমাবদ্ধ করে - এর পরিবর্তে Ctrl+V ব্যবহার করে দেখুন, অথবা HTTPS-এর মাধ্যমে Termix পরিবেশন করুন।", + "closeTab": "বন্ধ করুন", + "connecting": "সংযোগ স্থাপন...", + "connectionLost": "সংযোগ বিচ্ছিন্ন", + "connectionRejected": "সার্ভার কর্তৃক সংযোগ প্রত্যাখ্যাত হয়েছে। অনুগ্রহ করে আপনার প্রমাণীকরণ এবং নেটওয়ার্ক কনফিগারেশন যাচাই করুন।", + "connectionTimeout": "সংযোগের সময়সীমা শেষ", + "enableAutoTmuxAction": "অটো-Tmux সক্রিয় করুন", + "hostKeyRejected": "SSH হোস্ট কী যাচাইকরণ প্রত্যাখ্যাত হয়েছে। সংযোগ বাতিল করা হয়েছে।", + "linkDialogCopy": "অনুলিপি", + "linkDialogOpen": "খোলা", + "linkDialogTitle": "লিঙ্ক খুলুন", + "maxReconnectAttemptsReached": "পুনঃসংযোগের সর্বোচ্চ প্রচেষ্টা শেষ হয়েছে", + "noHostSelected": "কোন হোস্ট নির্বাচন করা হয়নি", + "opksshAuthFailed": "প্রমাণীকরণ ব্যর্থ হয়েছে। অনুগ্রহ করে আপনার পরিচয়পত্র যাচাই করে আবার চেষ্টা করুন।", + "opksshTimeout": "প্রমাণীকরণের সময়সীমা শেষ হয়ে গেছে। অনুগ্রহ করে আবার চেষ্টা করুন।", + "passwordPromptFillTitle": "সংরক্ষিত পাসওয়ার্ডটি এই প্রম্পটে পূরণ করুন?", + "reconnect": "পুনরায় সংযোগ করুন", + "reconnected": "সফলভাবে পুনরায় সংযুক্ত হয়েছে", + "reconnecting": "পুনরায় সংযোগ স্থাপন করা হচ্ছে... ({{attempt}}/{{max}})", + "searchCaseSensitive": "দেশলাইয়ের বাক্স", + "searchClose": "বন্ধ করুন (পলায়ন)", + "searchNext": "পরবর্তী ম্যাচ (প্রবেশ করুন)", + "searchNoResults": "কোন ফলাফল নেই", + "searchPlaceholder": "খুঁজুন", + "searchPrevious": "পূর্ববর্তী ম্যাচ (Shift+Enter)", + "searchRegex": "রেগুলার এক্সপ্রেশন ব্যবহার করুন", + "searchResultCount": "{{index}} এর {{count}}", + "searchWholeWord": "সম্পূর্ণ শব্দ মেলান", + "sessionExpiredNotice": "{{host}} -এ আপনার পূর্ববর্তী সেশনটি {{minutes}} মিনিট পর সংযোগ বিচ্ছিন্ন হয়ে মেয়াদোত্তীর্ণ হয়েছে; শেল এবং এর মধ্যে চলমান সবকিছু চলে গেছে। সেশনগুলো সচল রাখতে এই হোস্টে অটো-টিমাক্স (Auto-Tmux) সক্রিয় করুন।", + "sessionExpiredNoticeNoMinutes": "{{host}} -এ আপনার পূর্ববর্তী সেশনটি সংযোগ বিচ্ছিন্ন থাকাকালীন মেয়াদোত্তীর্ণ হয়ে গেছে; শেল এবং এর মধ্যে চলমান সবকিছু মুছে গেছে। সেশনগুলো সচল রাখতে এই হোস্টে অটো-টিমাক্স (Auto-Tmux) সক্রিয় করুন।", + "sessionTakenOver": "সেশনটি অন্য একটি ট্যাবে খোলা হয়েছিল। পুনরায় সংযোগ করা হচ্ছে...", + "tmuxAttached": "সংযুক্ত ক্লায়েন্ট", + "tmuxAttachedCount": "{{count}} সংযুক্ত", + "tmuxCopyHint": "নির্বাচনটি ঠিক করুন এবং ক্লিপবোর্ডে কপি করতে এন্টার চাপুন।", + "tmuxCreateNew": "নতুন সেশন শুরু করুন", + "tmuxDetached": "tmux সেশন থেকে বিচ্ছিন্ন", + "tmuxLastActivity": "শেষ কার্যকলাপ", + "tmuxSessionAttached": "tmux সেশন সংযুক্ত হয়েছে: {{name}}", + "tmuxSessionCreated": "tmux সেশন তৈরি হয়েছে: {{name}}", + "tmuxSessionPickerDesc": "এই হোস্টে বিদ্যমান tmux সেশন পাওয়া গেছে। পুনরায় সংযুক্ত হতে বা একটি নতুন সেশন তৈরি করতে যেকোনো একটি নির্বাচন করুন।", + "tmuxSessionPickerTitle": "tmux সেশন", + "tmuxTimeDays": "{{count}}দিন আগে", + "tmuxTimeHours": "{{count}}ঘণ্টা আগে", + "tmuxTimeJustNow": "এইমাত্র", + "tmuxTimeMinutes": "{{count}}মিনিট আগে", + "tmuxUnavailable": "রিমোট হোস্টে tmux ইনস্টল করা নেই, তাই স্ট্যান্ডার্ড শেল ব্যবহার করা হচ্ছে।", + "tmuxWindowCount": "{{count}} জানালা", + "tmuxWindows": "উইন্ডোজ", + "unknownError": "অজানা ত্রুটি ঘটেছে", + "vaultAuthDescription": "হ্যাশিকর্প ভল্টে সাইন ইন করার জন্য একটি উইন্ডো খুলেছে। সেখানে সাইন-ইন সম্পন্ন করুন; এই সংযোগটি স্বয়ংক্রিয়ভাবে চলতে থাকবে।", + "vaultAuthFailed": "ভল্ট প্রমাণীকরণ ব্যর্থ হয়েছে। অনুগ্রহ করে আবার চেষ্টা করুন।", + "vaultAuthTitle": "ভল্টে সাইন-ইন করা আবশ্যক", + "vaultReopen": "সাইন-ইন উইন্ডো পুনরায় খুলুন", + "websocketAbnormalClose": "সংযোগটি অপ্রত্যাশিতভাবে বন্ধ হয়ে গেছে। এটি রিভার্স প্রক্সি বা SSL কনফিগারেশন সংক্রান্ত সমস্যার কারণে হতে পারে। অনুগ্রহ করে সার্ভার লগ পরীক্ষা করুন।", + "websocketError": "ওয়েবসকেট সংযোগ ত্রুটি" + }, + "terminalToolbar": { + "detachTmux": "tmux বিচ্ছিন্ন করুন", + "detachTmuxDescription": "tmux সেশন বিচ্ছিন্ন করুন", + "dismissError": "ছবির ত্রুটি খারিজ করুন", + "hideToolbar": "টুলবার লুকান", + "image": "ছবির কার্যকলাপ (শুধুমাত্র ছবি)", + "imageActionFailed": "ছবির অ্যাকশন ব্যর্থ হয়েছে", + "layout": "টুলবার প্রদর্শন মোড", + "layoutExpanded": "মেট্রিক্স দিয়ে প্রসারিত", + "layoutIcon": "শুধুমাত্র আইকন", + "layoutLabeled": "আইকন এবং লেবেল", + "moveToolbar": "টুলবার সরান", + "moveToolbarHint": "টুলবারের অবস্থান পরিবর্তন করতে টেনে আনুন", + "paste": "ছবি পেস্ট করুন", + "pasteImage": "ক্লিপবোর্ড থেকে ছবি পেস্ট করুন (শুধুমাত্র ছবি)", + "retry": "পুনরায় চেষ্টা করুন", + "showToolbar": "টুলবার প্রসারিত করুন", + "upload": "ছবি আপলোড করুন", + "uploadImage": "হোস্টে একটি ছবি আপলোড করুন", + "uploadingImage": "আপলোড হচ্ছে..." + }, + "history": { + "noTerminalSelected": "কোন টার্মিনাল নির্বাচন করা হয়নি", + "noTerminalSelectedHint": "এর কমান্ড হিস্ট্রি দেখতে একটি SSH টার্মিনাল ট্যাব খুলুন।", + "searchPlaceholder": "অনুসন্ধানের ইতিহাস...", + "clearAll": "সব পরিষ্কার করুন", + "noHistoryEntries": "ইতিহাসের কোন এন্ট্রি নেই", + "trackingDisabled": "ইতিহাস ট্র্যাকিং নিষ্ক্রিয় করা হয়েছে" + }, + "touchInput": { + "enabled": "টাচ ইনপুট সক্রিয় করুন", + "enabledDesc": "টার্মিনালে এক আঙুলের স্পর্শে স্ক্রোলিং করার অনুমতি দিন", + "momentumEnabled": "গতি সঞ্চার করুন", + "momentumEnabledDesc": "সোয়াইপ করার পর কিছুক্ষণ স্ক্রোল করতে থাকুন।", + "advanced": "উন্নত", + "resetDefaults": "ডিফল্টে রিসেট করুন", + "dragThreshold": "ড্র্যাগ থ্রেশহোল্ড", + "maxWheelDelta": "সর্বোচ্চ চাকার ডেল্টা", + "momentumSampleWindow": "ভরবেগ নমুনা উইন্ডো", + "releaseGracePeriod": "মুক্তির জন্য অতিরিক্ত সময়সীমা", + "minimumVelocity": "সর্বনিম্ন বেগ", + "maximumVelocity": "সর্বোচ্চ বেগ", + "maximumDuration": "সর্বোচ্চ সময়কাল", + "maximumTravel": "সর্বোচ্চ ভ্রমণ", + "decayTime": "ক্ষয়ের সময়", + "pixelsPerTick": "প্রতি টিক পিক্সেল", + "maximumFrameInterval": "সর্বোচ্চ ফ্রেম ব্যবধান", + "maximumTicksPerFrame": "প্রতি ফ্রেমে সর্বোচ্চ টিক" + }, + "imageStorage": { + "instanceId": "টার্মিনাল সেশন ইনস্ট্যান্স আইডি", + "instanceIdDesc": "আপনার সংযুক্ত টার্মিনাল ট্যাবগুলোর একটির ইনস্ট্যান্স আইডি, যা দৃশ্যমানতা পরীক্ষা করতে ব্যবহৃত হয়।", + "selectedModeUnavailable": "অনুপলব্ধ", + "test": "পরীক্ষা", + "testing": "পরীক্ষা চলছে...", + "testFailed": "ইমেজ স্টোরেজ পরীক্ষা করতে ব্যর্থ হয়েছে", + "testConnected": "টার্মিনাল সেশন সংযুক্ত আছে।", + "testNotConnected": "এই ইনস্ট্যান্স আইডির সাথে কোনো সংযুক্ত টার্মিনাল সেশন নেই।", + "testSelectedMode": "আপলোড করতে ব্যবহৃত হবে: {{mode}}", + "testLocalVisible": "সংযুক্ত হোস্ট থেকে স্থানীয় ডিরেক্টরিটি দেখা যায়।", + "testLocalNotVisible": "সংযুক্ত হোস্ট থেকে স্থানীয় ডিরেক্টরিটি দেখা যাচ্ছে না।" + }, + "settings": { + "admin": { + "group": { + "touch": "টাচ ইনপুট", + "images": "ইমেজ স্টোরেজ" + }, + "sessionTimeoutMinutes": { + "label": "টার্মিনাল সেশন স্থায়িত্ব", + "description": "একটি সংযোগ বিচ্ছিন্ন টার্মিনাল বন্ধ হওয়ার আগে সার্ভারে কতক্ষণ চালু থাকে। সর্বনিম্ন ১ · সর্বোচ্চ ১৪৪০ মিনিট। যে সেশনগুলোকে এই সময়ের চেয়ে বেশি সময় ধরে চালু রাখতে হবে, সেগুলোর জন্য হোস্টে অটো-টিমাক্স (Auto-Tmux) সক্রিয় করুন।" + }, + "commandHistoryEnabled": { + "label": "কমান্ডের ইতিহাস", + "description": "কমান্ড হিস্ট্রি রেকর্ডিংয়ের অনুমতি দিন। এটি নিষ্ক্রিয় করা থাকলে, হোস্ট-ভিত্তিক সেটিংস নির্বিশেষে হিস্ট্রি সংরক্ষিত হয় না।" + }, + "imageStorageMode": { + "label": "স্টোরেজ মোড", + "description": "যেখানে পেস্ট করা এবং আপলোড করা টার্মিনাল ইমেজগুলো লেখা হয়। কানেকশন অনুযায়ী অটো পিক, লোকাল এবং রিমোট (SFTP) নির্দিষ্ট করা আছে।", + "auto": "অটো", + "local": "স্থানীয়", + "remoteSftp": "রিমোট (এসএফটিপি)" + }, + "imageLocalDir": { + "label": "স্থানীয় ডিরেক্টরি" + }, + "imageHostPath": { + "label": "হোস্ট পাথ", + "description": "টার্মিনাল এজেন্টরা স্থানীয় ডিরেক্টরির যে হোস্ট-সাইড ভিউটি পায়।" + }, + "imageTtlMs": { + "label": "টিটিএল (মিলিসেকেন্ড)" + }, + "imageMaxCount": { + "label": "সর্বোচ্চ ছবির সংখ্যা" + }, + "imageMaxBytes": { + "label": "সর্বোচ্চ স্টোরেজ বাইট" + } + }, + "host": { + "enableTerminalToolbar": { + "label": "টার্মিনাল টুলবার সক্রিয় করুন", + "description": "এই হোস্টের টুলস এবং লাইভ পরিসংখ্যানের জন্য সক্রিয় টার্মিনালে ডক করা একটি কুইক-অ্যাক্সেস টুলবার দেখান।" + }, + "enableCommandHistory": { + "label": "কমান্ডের ইতিহাস", + "description": "ইতিহাস এবং স্বয়ংক্রিয় সম্পূর্ণতার জন্য এই টার্মিনালে চালানো কমান্ডগুলো রেকর্ড করুন।" + }, + "enableTerminal": { + "label": "টার্মিনাল সক্রিয় করুন" + } + } + }, + "palette": { + "localTerminal": "স্থানীয় টার্মিনাল" + } +} diff --git a/plugins/ssh-terminal/locales/translated/ca_ES.json b/plugins/ssh-terminal/locales/translated/ca_ES.json new file mode 100644 index 000000000..7475835af --- /dev/null +++ b/plugins/ssh-terminal/locales/translated/ca_ES.json @@ -0,0 +1,203 @@ +{ + "mobileKeyboard": { + "addQuickKey": "Afegeix", + "arrowDown": "Fletxa avall", + "arrowLeft": "Fletxa esquerra", + "arrowRight": "Fletxa dreta", + "arrowUp": "Fletxa amunt", + "backTab": "⇥", + "ctrl": "Ctrl", + "delete": "Eliminar", + "done": "Fet", + "editQuickKeys": "Edita les tecles ràpides", + "end": "Final", + "esc": "Esc", + "home": "Inici", + "pageDown": "Av Pàg", + "pageUp": "Re Pàg", + "paste": "Enganxa", + "quickKeyPlaceholder": "p. ex. sudo ", + "quickKeysDesc": "Toca × per eliminar. Admet fins a 8 caràcters.", + "quickKeysTitle": "Tecles ràpides", + "removeQuickKey": "Elimina", + "resetDefaults": "Restableix els valors predeterminats", + "shift": "Majúscules", + "tab": "Pestanya" + }, + "terminal": { + "autoTmuxEnableFailed": "No s'ha pogut habilitar Auto-Tmux", + "autoTmuxEnabled": "Auto-Tmux habilitat per a {{host}}. S'aplica a partir de la següent connexió.", + "clipboardHttpWarning": "No s'ha pogut llegir el porta-retalls. Alguns navegadors restringeixen l'accés al porta-retalls fora d'HTTPS; proveu Ctrl+V o serviu Termix a través d'HTTPS.", + "closeTab": "Tanca", + "connecting": "Connectant...", + "connectionLost": "Connexió perduda", + "connectionRejected": "Connexió rebutjada pel servidor. Si us plau, comproveu l'autenticació i la configuració de xarxa.", + "connectionTimeout": "Temps d'espera de connexió", + "enableAutoTmuxAction": "Activa la transmissió automàtica", + "hostKeyRejected": "Verificació de la clau de l'amfitrió SSH rebutjada. Connexió cancel·lada.", + "linkDialogCopy": "Còpia", + "linkDialogOpen": "Obre", + "linkDialogTitle": "Obre l'enllaç", + "maxReconnectAttemptsReached": "S'ha arribat al màxim d'intents de reconnexió", + "noHostSelected": "No s'ha seleccionat cap amfitrió", + "opksshAuthFailed": "L'autenticació ha fallat. Si us plau, comproveu les vostres credencials i torneu-ho a intentar.", + "opksshTimeout": "S'ha esgotat el temps d'autenticació. Torna-ho a provar.", + "passwordPromptFillTitle": "Voleu introduir la contrasenya desada en aquesta sol·licitud?", + "reconnect": "Reconnecta", + "reconnected": "S'ha reconnectat correctament", + "reconnecting": "Reconnectant... ({{attempt}}/{{max}})", + "searchCaseSensitive": "Coincideix amb el cas", + "searchClose": "Tanca (Escapada)", + "searchNext": "Pròxima coincidència (Intro)", + "searchNoResults": "Sense resultats", + "searchPlaceholder": "Troba", + "searchPrevious": "Coincidència anterior (Maj+Intro)", + "searchRegex": "Utilitza l'expressió regular", + "searchResultCount": "{{index}} de {{count}}", + "searchWholeWord": "Coincideix amb la paraula sencera", + "sessionExpiredNotice": "La sessió anterior a {{host}} ha caducat després de {{minutes}} minuts de desconnexió; l'intèrpret d'ordres i tot el que s'hi executa ha desaparegut. Activeu Auto-Tmux en aquest amfitrió per mantenir les sessions actives.", + "sessionExpiredNoticeNoMinutes": "La sessió anterior a {{host}} ha caducat mentre estava desconnectada; l'intèrpret d'ordres i tot el que s'hi executa ha desaparegut. Activeu Auto-Tmux en aquest host per mantenir les sessions actives.", + "sessionTakenOver": "La sessió s'ha obert en una altra pestanya. S'està tornant a connectar...", + "tmuxAttached": "Clients adjunts", + "tmuxAttachedCount": "{{count}} adjunt", + "tmuxCopyHint": "Ajusta la selecció i prem Intro per copiar al porta-retalls", + "tmuxCreateNew": "Inicia una nova sessió", + "tmuxDetached": "Desconnectat de la sessió tmux", + "tmuxLastActivity": "Darrera activitat", + "tmuxSessionAttached": "sessió tmux adjunta: {{name}}", + "tmuxSessionCreated": "Sessió tmux creada: {{name}}", + "tmuxSessionPickerDesc": "S'han trobat sessions tmux existents en aquest amfitrió. Seleccioneu-ne una per tornar-la a adjuntar o crear una sessió nova.", + "tmuxSessionPickerTitle": "Sessions de tmux", + "tmuxTimeDays": "fa {{count}}dies", + "tmuxTimeHours": "fa {{count}}h", + "tmuxTimeJustNow": "ara mateix", + "tmuxTimeMinutes": "fa {{count}}minuts", + "tmuxUnavailable": "tmux no està instal·lat a l'amfitrió remot, tornant a l'intèrpret d'ordres estàndard", + "tmuxWindowCount": "finestra {{count}}", + "tmuxWindows": "Finestres", + "unknownError": "S'ha produït un error desconegut", + "vaultAuthDescription": "S'ha obert una finestra per iniciar la sessió a HashiCorp Vault. Completeu l'inici de sessió allà; aquesta connexió continuarà automàticament.", + "vaultAuthFailed": "L'autenticació de la caixa forta ha fallat. Torna-ho a provar.", + "vaultAuthTitle": "Cal iniciar sessió a la caixa forta", + "vaultReopen": "Torna a obrir la finestra d'inici de sessió", + "websocketAbnormalClose": "La connexió s'ha tancat inesperadament. Això pot ser degut a un problema de configuració del proxy invers o de l'SSL. Si us plau, reviseu els registres del servidor.", + "websocketError": "Error de connexió de WebSocket" + }, + "terminalToolbar": { + "detachTmux": "Desconnecta tmux", + "detachTmuxDescription": "Desconnecta la sessió tmux", + "dismissError": "Error d'ignorar la imatge", + "hideToolbar": "Amaga la barra d'eines", + "image": "Accions d'imatge (només imatges)", + "imageActionFailed": "L'acció de la imatge ha fallat", + "layout": "Mode de visualització de la barra d'eines", + "layoutExpanded": "Ampliat amb mètriques", + "layoutIcon": "Només icona", + "layoutLabeled": "Icones i etiquetes", + "moveToolbar": "Mou la barra d'eines", + "moveToolbarHint": "Arrossega per reposicionar la barra d'eines", + "paste": "Enganxa la imatge", + "pasteImage": "Enganxa la imatge del porta-retalls (només imatges)", + "retry": "Torna-ho a intentar", + "showToolbar": "Expandeix la barra d'eines", + "upload": "Puja una imatge", + "uploadImage": "Puja una imatge a l'amfitrió", + "uploadingImage": "S'està carregant..." + }, + "history": { + "noTerminalSelected": "No s'ha seleccionat cap terminal", + "noTerminalSelectedHint": "Obriu una pestanya de terminal SSH per veure el seu historial d'ordres", + "searchPlaceholder": "Historial de cerca...", + "clearAll": "Esborra-ho tot", + "noHistoryEntries": "Sense entrades a l'historial", + "trackingDisabled": "El seguiment de l'historial està desactivat" + }, + "touchInput": { + "enabled": "Activa l'entrada tàctil", + "enabledDesc": "Permetre el desplaçament tàctil amb un dit als terminals", + "momentumEnabled": "Activa l'impuls", + "momentumEnabledDesc": "Continua desplaçant-te breument després de lliscar el dit", + "advanced": "Avançat", + "resetDefaults": "Restableix els valors predeterminats", + "dragThreshold": "Llindar d'arrossegament", + "maxWheelDelta": "Delta màxim de roda", + "momentumSampleWindow": "Finestra de mostra de momentum", + "releaseGracePeriod": "Període de gràcia de llançament", + "minimumVelocity": "Velocitat mínima", + "maximumVelocity": "Velocitat màxima", + "maximumDuration": "Durada màxima", + "maximumTravel": "Recorregut màxim", + "decayTime": "Temps de decadència", + "pixelsPerTick": "Píxels per clic", + "maximumFrameInterval": "Interval màxim de fotograma", + "maximumTicksPerFrame": "Màxims de tics per fotograma" + }, + "imageStorage": { + "instanceId": "ID d'instància de sessió de terminal", + "instanceIdDesc": "ID d'instància d'una de les pestanyes del terminal connectat, que s'utilitza per provar la visibilitat.", + "selectedModeUnavailable": "No disponible", + "test": "Prova", + "testing": "Proves...", + "testFailed": "No s'ha pogut provar l'emmagatzematge d'imatges", + "testConnected": "La sessió de terminal està connectada.", + "testNotConnected": "No hi ha cap sessió de terminal connectada amb aquest ID d'instància.", + "testSelectedMode": "Una càrrega utilitzaria: {{mode}}", + "testLocalVisible": "El directori local és visible des de l'amfitrió connectat.", + "testLocalNotVisible": "El directori local NO és visible des de l'amfitrió connectat." + }, + "settings": { + "admin": { + "group": { + "touch": "Entrada tàctil", + "images": "Emmagatzematge d'imatges" + }, + "sessionTimeoutMinutes": { + "label": "Persistència de la sessió terminal", + "description": "Quant de temps es manté actiu un terminal desconnectat al servidor abans de tancar-lo. Mín 1 · Màx 1440 minuts. Per a sessions que han de durar més temps, activeu Auto-Tmux a l'amfitrió." + }, + "commandHistoryEnabled": { + "label": "Historial de comandaments", + "description": "Permet l'enregistrament de l'historial d'ordres. Quan està desactivat, l'historial no es desa independentment de la configuració de cada host." + }, + "imageStorageMode": { + "label": "Mode d'emmagatzematge", + "description": "On s'escriuen les imatges de terminal enganxades i carregades. Les seleccions automàtiques per connexió, Local i Remota (SFTP) estan corregides.", + "auto": "Automàtic", + "local": "Local", + "remoteSftp": "Remot (SFTP)" + }, + "imageLocalDir": { + "label": "Directori local" + }, + "imageHostPath": { + "label": "Camí d'amfitrió", + "description": "La vista del costat de l'amfitrió del directori local que reben els agents del terminal." + }, + "imageTtlMs": { + "label": "TTL (ms)" + }, + "imageMaxCount": { + "label": "Nombre màxim d'imatges" + }, + "imageMaxBytes": { + "label": "Bytes d'emmagatzematge màxims" + } + }, + "host": { + "enableTerminalToolbar": { + "label": "Habilita la barra d'eines del terminal", + "description": "Mostra una barra d'eines d'accés ràpid acoblada al terminal actiu per a les eines i les estadístiques en directe d'aquest amfitrió" + }, + "enableCommandHistory": { + "label": "Historial de comandaments", + "description": "Enregistra les ordres executades en aquest terminal per a l'historial i l'autocompleció" + }, + "enableTerminal": { + "label": "Habilita el terminal" + } + } + }, + "palette": { + "localTerminal": "Terminal local" + } +} diff --git a/plugins/ssh-terminal/locales/translated/cs_CZ.json b/plugins/ssh-terminal/locales/translated/cs_CZ.json new file mode 100644 index 000000000..6c17fd181 --- /dev/null +++ b/plugins/ssh-terminal/locales/translated/cs_CZ.json @@ -0,0 +1,203 @@ +{ + "mobileKeyboard": { + "addQuickKey": "Přidat", + "arrowDown": "Šipka dolů", + "arrowLeft": "Šipka vlevo", + "arrowRight": "Šipka vpravo", + "arrowUp": "Šipka nahoru", + "backTab": "⇥", + "ctrl": "Ctrl", + "delete": "Del", + "done": "Hotovo", + "editQuickKeys": "Upravit rychlé klávesy", + "end": "Konec", + "esc": "Esc", + "home": "Domov", + "pageDown": "PgDn", + "pageUp": "PgUp", + "paste": "Pasta", + "quickKeyPlaceholder": "např. sudo ", + "quickKeysDesc": "Klepnutím na × odstraníte. Podporuje až 8 znaků.", + "quickKeysTitle": "Rychlé klávesy", + "removeQuickKey": "Odstranit", + "resetDefaults": "Obnovit na výchozí nastavení", + "shift": "Posun", + "tab": "Záložka" + }, + "terminal": { + "autoTmuxEnableFailed": "Nepodařilo se povolit Auto-Tmux", + "autoTmuxEnabled": "Auto-Tmux povolen pro {{host}}. Platí od dalšího připojení.", + "clipboardHttpWarning": "Nelze přečíst schránku. Některé prohlížeče omezují přístup ke schránce mimo HTTPS – zkuste Ctrl+V nebo přes HTTPS poskytněte Termix.", + "closeTab": "Blízko", + "connecting": "Spojovací...", + "connectionLost": "Spojení ztraceno", + "connectionRejected": "Server odmítl připojení. Zkontrolujte prosím ověření a konfiguraci sítě.", + "connectionTimeout": "Časový limit připojení", + "enableAutoTmuxAction": "Povolit Auto-Tmux", + "hostKeyRejected": "Ověření klíče hostitele SSH odmítnuto. Připojení zrušeno.", + "linkDialogCopy": "Kopie", + "linkDialogOpen": "OTEVŘENO", + "linkDialogTitle": "Otevřít odkaz", + "maxReconnectAttemptsReached": "Dosažen maximální počet pokusů o opětovné připojení", + "noHostSelected": "Nebyl vybrán žádný hostitel", + "opksshAuthFailed": "Ověření se nezdařilo. Zkontrolujte prosím své přihlašovací údaje a zkuste to znovu.", + "opksshTimeout": "Časový limit ověření vypršel. Zkuste to prosím znovu.", + "passwordPromptFillTitle": "Vyplňte uložené heslo do tohoto řádku?", + "reconnect": "Znovu se připojte", + "reconnected": "Úspěšné opětovné připojení", + "reconnecting": "Znovu se připojuji... ({{attempt}}/{{max}})", + "searchCaseSensitive": "Zápasová karta", + "searchClose": "Zavřít (Escape)", + "searchNext": "Další zápas (Enter)", + "searchNoResults": "Žádné výsledky", + "searchPlaceholder": "Nalézt", + "searchPrevious": "Předchozí shoda (Shift+Enter)", + "searchRegex": "Použít regulární výraz", + "searchResultCount": "{{index}} z {{count}}", + "searchWholeWord": "Shoda celého slova", + "sessionExpiredNotice": "Vaše předchozí relace na {{host}} vypršela po {{minutes}} minutách odpojení; shell a vše, co v něm běží, je pryč. Povolte na tomto hostiteli Auto-Tmux, aby relace zůstaly aktivní.", + "sessionExpiredNoticeNoMinutes": "Vaše předchozí relace na {{host}} vypršela během odpojení; shell a vše, co v něm běží, je pryč. Povolte na tomto hostiteli Auto-Tmux, aby relace zůstaly aktivní.", + "sessionTakenOver": "Relace byla otevřena na jiné kartě. Znovu se připojuji...", + "tmuxAttached": "Připojení klienti", + "tmuxAttachedCount": "{{count}} připojen", + "tmuxCopyHint": "Upravte výběr a stiskněte Enter pro zkopírování do schránky", + "tmuxCreateNew": "Zahájit novou relaci", + "tmuxDetached": "Odpojeno od relace tmux", + "tmuxLastActivity": "Poslední aktivita", + "tmuxSessionAttached": "připojena relace tmux: {{name}}", + "tmuxSessionCreated": "Vytvořena relace tmux: {{name}}", + "tmuxSessionPickerDesc": "Na tomto hostiteli byly nalezeny existující relace tmux. Vyberte jednu, kterou chcete znovu připojit, nebo vytvořte novou relaci.", + "tmuxSessionPickerTitle": "Sezení tmux", + "tmuxTimeDays": "Před {{count}}dny", + "tmuxTimeHours": "{{count}}před hodinami", + "tmuxTimeJustNow": "právě teď", + "tmuxTimeMinutes": "{{count}}před m", + "tmuxUnavailable": "tmux není nainstalován na vzdáleném hostiteli, vrací se ke standardnímu shellu.", + "tmuxWindowCount": "{{count}} okno", + "tmuxWindows": "Okna", + "unknownError": "Došlo k neznámé chybě", + "vaultAuthDescription": "Otevřelo se okno pro přihlášení do HashiCorp Vault. Dokončete přihlášení tam; toto připojení bude automaticky pokračovat.", + "vaultAuthFailed": "Ověření v trezoru se nezdařilo. Zkuste to prosím znovu.", + "vaultAuthTitle": "Vyžaduje se přihlášení do trezoru", + "vaultReopen": "Znovu otevřít přihlašovací okno", + "websocketAbnormalClose": "Spojení se neočekávaně ukončilo. Může to být způsobeno problémem s reverzní proxy nebo konfigurací SSL. Zkontrolujte prosím protokoly serveru.", + "websocketError": "Chyba připojení k WebSocketu" + }, + "terminalToolbar": { + "detachTmux": "Odpojit tmux", + "detachTmuxDescription": "Odpojení relace tmux", + "dismissError": "Chyba při zavírání obrázku", + "hideToolbar": "Skrýt panel nástrojů", + "image": "Akce s obrázky (pouze obrázky)", + "imageActionFailed": "Akce s obrázkem se nezdařila", + "layout": "Režim zobrazení panelu nástrojů", + "layoutExpanded": "Rozšířeno o metriky", + "layoutIcon": "Pouze ikona", + "layoutLabeled": "Ikony a popisky", + "moveToolbar": "Přesunout panel nástrojů", + "moveToolbarHint": "Přetažením změňte umístění panelu nástrojů", + "paste": "Vložit obrázek", + "pasteImage": "Vložit obrázek ze schránky (pouze obrázky)", + "retry": "Zkusit znovu", + "showToolbar": "Rozbalit panel nástrojů", + "upload": "Nahrát obrázek", + "uploadImage": "Nahrajte obrázek na hostitele", + "uploadingImage": "Nahrávání..." + }, + "history": { + "noTerminalSelected": "Není vybrán žádný terminál", + "noTerminalSelectedHint": "Otevření karty terminálu SSH pro zobrazení historie jejích příkazů", + "searchPlaceholder": "Historie vyhledávání...", + "clearAll": "Vymazat vše", + "noHistoryEntries": "Žádné záznamy v historii", + "trackingDisabled": "Sledování historie je zakázáno" + }, + "touchInput": { + "enabled": "Povolit dotykový vstup", + "enabledDesc": "Povolit posouvání jedním prstem v terminálech", + "momentumEnabled": "Povolit hybnost", + "momentumEnabledDesc": "Po přejetí prstem krátce pokračujte v rolování", + "advanced": "Moderní", + "resetDefaults": "Obnovit na výchozí nastavení", + "dragThreshold": "Práh přetažení", + "maxWheelDelta": "Maximální delta kola", + "momentumSampleWindow": "Okno vzorku hybnosti", + "releaseGracePeriod": "Dodatečná lhůta", + "minimumVelocity": "Minimální rychlost", + "maximumVelocity": "Maximální rychlost", + "maximumDuration": "Maximální doba trvání", + "maximumTravel": "Maximální zdvih", + "decayTime": "Doba rozpadu", + "pixelsPerTick": "Pixely na tik", + "maximumFrameInterval": "Maximální interval snímků", + "maximumTicksPerFrame": "Maximální počet tiků na snímek" + }, + "imageStorage": { + "instanceId": "ID instance terminálové relace", + "instanceIdDesc": "ID instance jedné z vašich připojených karet terminálu, používané k testování viditelnosti.", + "selectedModeUnavailable": "Nedostupné", + "test": "Test", + "testing": "Testování...", + "testFailed": "Nepodařilo se otestovat úložiště obrázků", + "testConnected": "Terminálová relace je připojena.", + "testNotConnected": "S tímto ID instance není připojena žádná terminálová relace.", + "testSelectedMode": "Nahrávání by použilo: {{mode}}", + "testLocalVisible": "Lokální adresář je viditelný z připojeného hostitele.", + "testLocalNotVisible": "Lokální adresář NENÍ viditelný z připojeného hostitele." + }, + "settings": { + "admin": { + "group": { + "touch": "Dotykový vstup", + "images": "Ukládání obrázků" + }, + "sessionTimeoutMinutes": { + "label": "Perzistence terminálové relace", + "description": "Jak dlouho je odpojený terminál udržován aktivní na serveru, než je uzavřen. Min. 1 · Max. 1440 minut. Pro relace, které musí tuto dobu přetrvávat, povolte na hostiteli Auto-Tmux." + }, + "commandHistoryEnabled": { + "label": "Historie příkazů", + "description": "Povolit záznam historie příkazů. Pokud je tato možnost zakázána, historie se neukládá bez ohledu na nastavení pro jednotlivé hostitele." + }, + "imageStorageMode": { + "label": "Režim úložiště", + "description": "Kam se zapisují vložené a nahrané obrazy terminálu. Automatické výběry podle připojení, lokální a vzdálené (SFTP) jsou opraveny.", + "auto": "Auto", + "local": "Místní", + "remoteSftp": "Vzdálené (SFTP)" + }, + "imageLocalDir": { + "label": "Místní adresář" + }, + "imageHostPath": { + "label": "Cesta hostitele", + "description": "Pohled na lokální adresář, který termináloví agenti přijímají, na straně hostitele." + }, + "imageTtlMs": { + "label": "Doba trvání (ms)" + }, + "imageMaxCount": { + "label": "Maximální počet obrázků" + }, + "imageMaxBytes": { + "label": "Maximální počet bajtů úložiště" + } + }, + "host": { + "enableTerminalToolbar": { + "label": "Povolit panel nástrojů terminálu", + "description": "Zobrazit panel nástrojů pro rychlý přístup ukotvený v aktivním terminálu pro nástroje a aktuální statistiky tohoto hostitele" + }, + "enableCommandHistory": { + "label": "Historie příkazů", + "description": "Zaznamenávejte příkazy spouštěné v tomto terminálu pro historii a automatické dokončování" + }, + "enableTerminal": { + "label": "Povolit terminál" + } + } + }, + "palette": { + "localTerminal": "Místní terminál" + } +} diff --git a/plugins/ssh-terminal/locales/translated/da_DK.json b/plugins/ssh-terminal/locales/translated/da_DK.json new file mode 100644 index 000000000..3a8bc1725 --- /dev/null +++ b/plugins/ssh-terminal/locales/translated/da_DK.json @@ -0,0 +1,203 @@ +{ + "mobileKeyboard": { + "addQuickKey": "Tilføje", + "arrowDown": "Pil ned", + "arrowLeft": "Pil til venstre", + "arrowRight": "Pil til højre", + "arrowUp": "Pil op", + "backTab": "⇥", + "ctrl": "Ctrl", + "delete": "Del", + "done": "Færdig", + "editQuickKeys": "Rediger genvejstaster", + "end": "Ende", + "esc": "Esc", + "home": "Hjem", + "pageDown": "PgDn", + "pageUp": "Side op", + "paste": "Indsæt", + "quickKeyPlaceholder": "f.eks. sudo ", + "quickKeysDesc": "Tryk på × for at fjerne. Understøtter op til 8 tegn.", + "quickKeysTitle": "Hurtigtaster", + "removeQuickKey": "Fjerne", + "resetDefaults": "Nulstil til standardindstillinger", + "shift": "Flytte", + "tab": "Faneblad" + }, + "terminal": { + "autoTmuxEnableFailed": "Kunne ikke aktivere Auto-Tmux", + "autoTmuxEnabled": "Auto-Tmux aktiveret for {{host}}. Det gælder fra den næste forbindelse.", + "clipboardHttpWarning": "Kunne ikke læse udklipsholderen. Nogle browsere begrænser adgang til udklipsholderen uden for HTTPS - prøv Ctrl+V i stedet, eller server Termix via HTTPS.", + "closeTab": "Tæt", + "connecting": "Forbinder...", + "connectionLost": "Forbindelse tabt", + "connectionRejected": "Forbindelsen blev afvist af serveren. Kontroller din godkendelse og netværkskonfiguration.", + "connectionTimeout": "Forbindelsestimeout", + "enableAutoTmuxAction": "Aktivér Auto-Tmux", + "hostKeyRejected": "SSH-værtsnøglebekræftelse afvist. Forbindelse afbrudt.", + "linkDialogCopy": "Kopi", + "linkDialogOpen": "Åben", + "linkDialogTitle": "Åbn link", + "maxReconnectAttemptsReached": "Maksimalt antal forsøg på genoprettelse nået", + "noHostSelected": "Ingen vært valgt", + "opksshAuthFailed": "Godkendelse mislykkedes. Tjek dine loginoplysninger, og prøv igen.", + "opksshTimeout": "Godkendelsen udløb. Prøv igen.", + "passwordPromptFillTitle": "Udfyld den gemte adgangskode i denne prompt?", + "reconnect": "Genopret forbindelse", + "reconnected": "Genoprettet forbindelse", + "reconnecting": "Genopretter forbindelse... ({{attempt}}/{{max}})", + "searchCaseSensitive": "Match-sag", + "searchClose": "Luk (Escape)", + "searchNext": "Næste kamp (Enter)", + "searchNoResults": "Ingen resultater", + "searchPlaceholder": "Finde", + "searchPrevious": "Forrige match (Shift+Enter)", + "searchRegex": "Brug regulært udtryk", + "searchResultCount": "{{index}} af {{count}}", + "searchWholeWord": "Match hele ordet", + "sessionExpiredNotice": "Din forrige session på {{host}} udløb efter {{minutes}} minutters afbrydelse; shell'en og alt, der kører i den, er væk. Aktiver Auto-Tmux på denne vært for at holde sessionerne aktive.", + "sessionExpiredNoticeNoMinutes": "Din forrige session på {{host}} udløb, mens forbindelsen var afbrudt; skallen og alt, der kørte i den, er væk. Aktiver Auto-Tmux på denne vært for at holde sessionerne aktive.", + "sessionTakenOver": "Sessionen blev åbnet i en anden fane. Genopretter forbindelse...", + "tmuxAttached": "Tilknyttede klienter", + "tmuxAttachedCount": "{{count}} vedhæftet", + "tmuxCopyHint": "Juster valget, og tryk på Enter for at kopiere til udklipsholderen", + "tmuxCreateNew": "Start en ny session", + "tmuxDetached": "Afkoblet fra tmux-session", + "tmuxLastActivity": "Sidste aktivitet", + "tmuxSessionAttached": "tmux-session vedhæftet: {{name}}", + "tmuxSessionCreated": "tmux-session oprettet: {{name}}", + "tmuxSessionPickerDesc": "Eksisterende tmux-sessioner fundet på denne vært. Vælg en for at gentilknytte eller opret en ny session.", + "tmuxSessionPickerTitle": "tmux-sessioner", + "tmuxTimeDays": "for {{count}}dage siden", + "tmuxTimeHours": "For {{count}}timer siden", + "tmuxTimeJustNow": "lige nu", + "tmuxTimeMinutes": "For {{count}}minutter siden", + "tmuxUnavailable": "tmux er ikke installeret på den eksterne vært, og bruger standardshell", + "tmuxWindowCount": "{{count}} vindue", + "tmuxWindows": "Vinduer", + "unknownError": "Der opstod en ukendt fejl", + "vaultAuthDescription": "Et vindue er åbnet, hvor du kan logge ind på HashiCorp Vault. Fuldfør loginprocessen der; forbindelsen fortsætter automatisk.", + "vaultAuthFailed": "Godkendelse af arkiv mislykkedes. Prøv igen.", + "vaultAuthTitle": "Login til Vault kræves", + "vaultReopen": "Genåbn loginvinduet", + "websocketAbnormalClose": "Forbindelsen blev lukket uventet. Dette kan skyldes et problem med reverse proxy eller SSL-konfigurationen. Tjek venligst serverlogfilerne.", + "websocketError": "WebSocket-forbindelsesfejl" + }, + "terminalToolbar": { + "detachTmux": "Afmonter tmux", + "detachTmuxDescription": "Afbryd tmux-session", + "dismissError": "Afvis billedfejl", + "hideToolbar": "Skjul værktøjslinjen", + "image": "Billedhandlinger (kun billeder)", + "imageActionFailed": "Billedhandling mislykkedes", + "layout": "Værktøjslinjevisningstilstand", + "layoutExpanded": "Udvidet med metrikker", + "layoutIcon": "Kun ikon", + "layoutLabeled": "Ikoner og etiketter", + "moveToolbar": "Flyt værktøjslinjen", + "moveToolbarHint": "Træk for at flytte værktøjslinjen", + "paste": "Indsæt billede", + "pasteImage": "Indsæt billede fra udklipsholder (kun billeder)", + "retry": "Prøv igen", + "showToolbar": "Udvid værktøjslinjen", + "upload": "Upload billede", + "uploadImage": "Upload et billede til værten", + "uploadingImage": "Uploader..." + }, + "history": { + "noTerminalSelected": "Ingen terminal valgt", + "noTerminalSelectedHint": "Åbn en SSH-terminalfane for at se dens kommandohistorik", + "searchPlaceholder": "Søgehistorik...", + "clearAll": "Ryd alt", + "noHistoryEntries": "Ingen historikposter", + "trackingDisabled": "Historiksporing er deaktiveret" + }, + "touchInput": { + "enabled": "Aktivér berøringsinput", + "enabledDesc": "Tillad scrolling med én finger i terminaler", + "momentumEnabled": "Aktiver momentum", + "momentumEnabledDesc": "Fortsæt med at scrolle kort efter et swipe", + "advanced": "Fremskreden", + "resetDefaults": "Nulstil til standardindstillinger", + "dragThreshold": "Trækgrænse", + "maxWheelDelta": "Maksimal hjuldelta", + "momentumSampleWindow": "Momentum-eksempelvindue", + "releaseGracePeriod": "Frigivelsesperiode", + "minimumVelocity": "Minimumshastighed", + "maximumVelocity": "Maksimal hastighed", + "maximumDuration": "Maksimal varighed", + "maximumTravel": "Maksimal rejse", + "decayTime": "Henfaldstid", + "pixelsPerTick": "Pixels pr. tick", + "maximumFrameInterval": "Maksimalt billedinterval", + "maximumTicksPerFrame": "Maksimalt antal ticks pr. frame" + }, + "imageStorage": { + "instanceId": "Terminalsessionsinstans-ID", + "instanceIdDesc": "Instans-ID for en af dine tilsluttede terminalfaner, der bruges til at teste synligheden.", + "selectedModeUnavailable": "Ikke tilgængelig", + "test": "Prøve", + "testing": "Testning...", + "testFailed": "Kunne ikke teste billedlagring", + "testConnected": "Terminalsessionen er forbundet.", + "testNotConnected": "Ingen forbundet terminalsession med dette instans-ID.", + "testSelectedMode": "En upload ville bruge: {{mode}}", + "testLocalVisible": "Den lokale mappe er synlig fra den tilsluttede vært.", + "testLocalNotVisible": "Den lokale mappe er IKKE synlig fra den tilsluttede vært." + }, + "settings": { + "admin": { + "group": { + "touch": "Berøringsinput", + "images": "Billedlagring" + }, + "sessionTimeoutMinutes": { + "label": "Terminalsessionens persistens", + "description": "Hvor længe en frakoblet terminal holdes aktiv på serveren, før den lukkes. Min. 1 · Maks. 1440 minutter. For sessioner, der skal overleve dette, skal du aktivere Auto-Tmux på værten." + }, + "commandHistoryEnabled": { + "label": "Kommandohistorik", + "description": "Tillad optagelse af kommandohistorik. Når den er deaktiveret, gemmes historikken ikke uanset indstillingerne for den enkelte vært." + }, + "imageStorageMode": { + "label": "Lagringstilstand", + "description": "Hvor indsatte og uploadede terminalbilleder skrives. Automatiske valg efter forbindelse, lokal og fjern (SFTP) er rettet.", + "auto": "Bil", + "local": "Lokal", + "remoteSftp": "Fjernbetjening (SFTP)" + }, + "imageLocalDir": { + "label": "Lokal mappe" + }, + "imageHostPath": { + "label": "Værtssti", + "description": "Værtsvisningen af den lokale mappe, som terminalagenter modtager." + }, + "imageTtlMs": { + "label": "TTL (ms)" + }, + "imageMaxCount": { + "label": "Maksimalt antal billeder" + }, + "imageMaxBytes": { + "label": "Maks. lagerplads i bytes" + } + }, + "host": { + "enableTerminalToolbar": { + "label": "Aktivér Terminalværktøjslinjen", + "description": "Vis en værktøjslinje med hurtig adgang, der er docket til den aktive terminal for denne værts værktøjer og live statistikker" + }, + "enableCommandHistory": { + "label": "Kommandohistorik", + "description": "Optagelseskommandoer køres i denne terminal for historik og autofuldførelse" + }, + "enableTerminal": { + "label": "Aktivér terminal" + } + } + }, + "palette": { + "localTerminal": "Lokal terminal" + } +} diff --git a/plugins/ssh-terminal/locales/translated/de_DE.json b/plugins/ssh-terminal/locales/translated/de_DE.json new file mode 100644 index 000000000..ff868dfbb --- /dev/null +++ b/plugins/ssh-terminal/locales/translated/de_DE.json @@ -0,0 +1,203 @@ +{ + "mobileKeyboard": { + "addQuickKey": "Hinzufügen", + "arrowDown": "Pfeil nach unten", + "arrowLeft": "Pfeil nach links", + "arrowRight": "Pfeil nach rechts", + "arrowUp": "Pfeil nach oben", + "backTab": "⇥", + "ctrl": "Strg", + "delete": "Entf", + "done": "Fertig", + "editQuickKeys": "Schnelltasten bearbeiten", + "end": "Ende", + "esc": "Esc", + "home": "Pos1", + "pageDown": "Bild ab", + "pageUp": "Bild auf", + "paste": "Paste", + "quickKeyPlaceholder": "z. B. sudo ", + "quickKeysDesc": "Tippen Sie auf × zum Entfernen. Unterstützt bis zu 8 Zeichen.", + "quickKeysTitle": "Schnelltasten", + "removeQuickKey": "Entfernen", + "resetDefaults": "Auf Standard zurücksetzen", + "shift": "Umschalt", + "tab": "Tab" + }, + "terminal": { + "autoTmuxEnableFailed": "Auto-Tmux konnte nicht aktiviert werden.", + "autoTmuxEnabled": "Auto-Tmux ist für {{host}}aktiviert. Es gilt ab der nächsten Verbindung.", + "clipboardHttpWarning": "Die Zwischenablage konnte nicht gelesen werden. Einige Browser beschränken den Zugriff auf die Zwischenablage außerhalb von HTTPS – versuchen Sie stattdessen Strg+V oder verwenden Sie für Termix eine HTTPS-Verbindung.", + "closeTab": "Schließen", + "connecting": "Verbinde...", + "connectionLost": "Verbindung verloren", + "connectionRejected": "Verbindung vom Server abgelehnt. Bitte überprüfen Sie Ihre Authentifizierung und Netzwerkkonfiguration.", + "connectionTimeout": "Verbindungs-Timeout", + "enableAutoTmuxAction": "Auto-Tmux aktivieren", + "hostKeyRejected": "SSH-Hostschlüssel-Überprüfung abgelehnt. Verbindung abgebrochen.", + "linkDialogCopy": "Kopieren", + "linkDialogOpen": "Öffnen", + "linkDialogTitle": "Link öffnen", + "maxReconnectAttemptsReached": "Maximale Wiederverbindungsversuche erreicht", + "noHostSelected": "Kein Host ausgewählt", + "opksshAuthFailed": "Authentifizierung fehlgeschlagen. Bitte überprüfen Sie Ihre Anmeldeinformationen und versuchen Sie es erneut.", + "opksshTimeout": "Authentifizierung abgelaufen. Bitte versuchen Sie es erneut.", + "passwordPromptFillTitle": "Gespeichertes Passwort in diese Eingabeaufforderung einfügen?", + "reconnect": "Erneut verbinden", + "reconnected": "Erfolgreich wieder verbunden", + "reconnecting": "Wiederverbinden... ({{attempt}}/{{max}})", + "searchCaseSensitive": "Streichholzschachtel", + "searchClose": "Schließen (Flucht)", + "searchNext": "Nächstes Spiel (Eingabe)", + "searchNoResults": "Keine Ergebnisse", + "searchPlaceholder": "Finden", + "searchPrevious": "Vorheriges Spiel (Umschalt+Eingabe)", + "searchRegex": "Regulären Ausdruck verwenden", + "searchResultCount": "{{index}} von {{count}}", + "searchWholeWord": "Gleiches Wort aus", + "sessionExpiredNotice": "Ihre vorherige Sitzung auf {{host}} ist nach {{minutes}} Minuten Verbindungsabbruch abgelaufen; die Shell und alle darin laufenden Prozesse wurden gelöscht. Aktivieren Sie Auto-Tmux auf diesem Host, um Sitzungen aufrechtzuerhalten.", + "sessionExpiredNoticeNoMinutes": "Ihre vorherige Sitzung auf {{host}} ist während der Trennung abgelaufen; die Shell und alle darin laufenden Prozesse wurden gelöscht. Aktivieren Sie Auto-Tmux auf diesem Host, um Sitzungen aufrechtzuerhalten.", + "sessionTakenOver": "Sitzung wurde in einem anderen Tab geöffnet. Verbindung wird wiederhergestellt...", + "tmuxAttached": "Verbundene Clients", + "tmuxAttachedCount": "{{count}} verbunden", + "tmuxCopyHint": "Auswahl anpassen und Enter drücken, um in die Zwischenablage zu kopieren", + "tmuxCreateNew": "Neue Sitzung starten", + "tmuxDetached": "Von tmux-Sitzung getrennt", + "tmuxLastActivity": "Letzte Aktivität", + "tmuxSessionAttached": "tmux-Sitzung verbunden: {{name}}", + "tmuxSessionCreated": "tmux-Sitzung erstellt: {{name}}", + "tmuxSessionPickerDesc": "Bestehende tmux-Sitzungen auf diesem Host gefunden. Wählen Sie eine aus, um sie erneut zu verbinden, oder erstellen Sie eine neue.", + "tmuxSessionPickerTitle": "tmux-Sitzungen", + "tmuxTimeDays": "vor {{count}} Tagen", + "tmuxTimeHours": "vor {{count}} Std.", + "tmuxTimeJustNow": "Soeben", + "tmuxTimeMinutes": "vor {{count}} Min.", + "tmuxUnavailable": "tmux ist auf dem Remote-Host nicht installiert, Fallback auf die Standard-Shell", + "tmuxWindowCount": "{{count}} Fenster", + "tmuxWindows": "Fenster", + "unknownError": "Unbekannter Fehler aufgetreten", + "vaultAuthDescription": "Ein Fenster zur Anmeldung bei HashiCorp Vault wurde geöffnet. Schließen Sie die Anmeldung dort ab; diese Verbindung wird automatisch fortgesetzt.", + "vaultAuthFailed": "Vault-Authentifizierung fehlgeschlagen. Bitte versuchen Sie es erneut.", + "vaultAuthTitle": "Vault-Anmeldung erforderlich", + "vaultReopen": "Anmeldefenster erneut öffnen", + "websocketAbnormalClose": "Verbindung unerwartet geschlossen. Dies kann auf ein Problem mit dem Reverse-Proxy oder der SSL-Konfiguration zurückzuführen sein. Bitte überprüfen Sie die Server-Logs.", + "websocketError": "WebSocket-Verbindungsfehler" + }, + "terminalToolbar": { + "detachTmux": "tmux trennen", + "detachTmuxDescription": "tmux-Sitzung trennen", + "dismissError": "Bildfehler ignorieren", + "hideToolbar": "Werkzeugleiste ausblenden", + "image": "Bildaktionen (nur Bilder)", + "imageActionFailed": "Bildaktion fehlgeschlagen", + "layout": "Symbolleistenanzeigemodus", + "layoutExpanded": "Erweitert um Kennzahlen", + "layoutIcon": "Nur Symbol", + "layoutLabeled": "Symbole und Beschriftungen", + "moveToolbar": "Verschieben Sie die Werkzeugleiste", + "moveToolbarHint": "Ziehen Sie, um die Symbolleiste neu zu positionieren.", + "paste": "Bild einfügen", + "pasteImage": "Bild aus der Zwischenablage einfügen (nur Bilder)", + "retry": "Wiederholen", + "showToolbar": "Symbolleiste erweitern", + "upload": "Bild hochladen", + "uploadImage": "Laden Sie ein Bild auf den Host hoch.", + "uploadingImage": "Wird hochgeladen..." + }, + "history": { + "noTerminalSelected": "Kein Terminal ausgewählt", + "noTerminalSelectedHint": "Öffnen Sie einen SSH-Terminal-Tab, um den Befehlsverlauf anzuzeigen.", + "searchPlaceholder": "Verlauf durchsuchen...", + "clearAll": "Alle löschen", + "noHistoryEntries": "Keine Verlaufseinträge", + "trackingDisabled": "Verlaufsaufzeichnung ist deaktiviert" + }, + "touchInput": { + "enabled": "Touch-Eingabe aktivieren", + "enabledDesc": "Ein-Finger-Touch-Scrollen in Terminals ermöglichen", + "momentumEnabled": "Dynamik aktivieren", + "momentumEnabledDesc": "Nach dem Wischen kurz weiterscrollen.", + "advanced": "Fortschrittlich", + "resetDefaults": "Auf Standardeinstellungen zurücksetzen", + "dragThreshold": "Widerstandsschwelle", + "maxWheelDelta": "Maximale Rad-Delta", + "momentumSampleWindow": "Momentum-Abtastfenster", + "releaseGracePeriod": "Freigabefrist", + "minimumVelocity": "Minimale Geschwindigkeit", + "maximumVelocity": "Maximale Geschwindigkeit", + "maximumDuration": "Maximale Dauer", + "maximumTravel": "Maximale Reisegeschwindigkeit", + "decayTime": "Zerfallszeit", + "pixelsPerTick": "Pixel pro Tick", + "maximumFrameInterval": "Maximales Frame-Intervall", + "maximumTicksPerFrame": "Maximale Anzahl an Ticks pro Frame" + }, + "imageStorage": { + "instanceId": "Terminal-Sitzungsinstanz-ID", + "instanceIdDesc": "Instanz-ID eines Ihrer verbundenen Terminal-Tabs, die zum Testen der Sichtbarkeit verwendet wird.", + "selectedModeUnavailable": "Nicht verfügbar", + "test": "Prüfen", + "testing": "Testen...", + "testFailed": "Fehler beim Testen der Bildspeicherung", + "testConnected": "Die Terminal-Sitzung ist verbunden.", + "testNotConnected": "Es besteht keine verbundene Terminal-Sitzung mit dieser Instanz-ID.", + "testSelectedMode": "Ein Upload würde Folgendes verwenden: {{mode}}", + "testLocalVisible": "Das lokale Verzeichnis ist vom verbundenen Host aus sichtbar.", + "testLocalNotVisible": "Das lokale Verzeichnis ist vom verbundenen Host aus nicht sichtbar." + }, + "settings": { + "admin": { + "group": { + "touch": "Berührungseingabe", + "images": "Bildspeicherung" + }, + "sessionTimeoutMinutes": { + "label": "Terminal-Sitzungspersistenz", + "description": "Wie lange ein getrenntes Terminal auf dem Server aktiv bleibt, bevor es geschlossen wird. Min. 1 Minute · Max. 1440 Minuten. Für Sitzungen, die länger als diese Zeitspanne bestehen bleiben sollen, aktivieren Sie Auto-Tmux auf dem Host." + }, + "commandHistoryEnabled": { + "label": "Befehlsverlauf", + "description": "Aufzeichnung des Befehlsverlaufs erlauben. Wenn deaktiviert, wird der Verlauf unabhängig von Host-Einstellungen nicht gespeichert." + }, + "imageStorageMode": { + "label": "Speichermodus", + "description": "Hier werden eingefügte und hochgeladene Terminalbilder gespeichert. Die automatische Auswahl anhand der Verbindung (lokal und remote (SFTP)) ist festgelegt.", + "auto": "Auto", + "local": "Lokal", + "remoteSftp": "Remote (SFTP)" + }, + "imageLocalDir": { + "label": "Lokales Verzeichnis" + }, + "imageHostPath": { + "label": "Hostpfad", + "description": "Die hostseitige Ansicht des lokalen Verzeichnisses, die Terminalagenten empfangen." + }, + "imageTtlMs": { + "label": "TTL (ms)" + }, + "imageMaxCount": { + "label": "Maximale Bildanzahl" + }, + "imageMaxBytes": { + "label": "Maximale Speicherkapazität in Bytes" + } + }, + "host": { + "enableTerminalToolbar": { + "label": "Terminal-Symbolleiste aktivieren", + "description": "Zeigt eine Schnellzugriffsleiste an, die am aktiven Terminal angedockt ist und die Tools und Live-Statistiken dieses Hosts anzeigt." + }, + "enableCommandHistory": { + "label": "Befehlsverlauf", + "description": "In diesem Terminal ausgeführte Befehle für Verlauf und Autovervollständigung aufzeichnen" + }, + "enableTerminal": { + "label": "Terminal aktivieren" + } + } + }, + "palette": { + "localTerminal": "Lokales Terminal" + } +} diff --git a/plugins/ssh-terminal/locales/translated/el_GR.json b/plugins/ssh-terminal/locales/translated/el_GR.json new file mode 100644 index 000000000..127e370db --- /dev/null +++ b/plugins/ssh-terminal/locales/translated/el_GR.json @@ -0,0 +1,203 @@ +{ + "mobileKeyboard": { + "addQuickKey": "Προσθέτω", + "arrowDown": "Βέλος προς τα κάτω", + "arrowLeft": "Βέλος αριστερά", + "arrowRight": "Βέλος δεξιά", + "arrowUp": "Βέλος προς τα πάνω", + "backTab": "⇥", + "ctrl": "Ctrl", + "delete": "Διαγραφή", + "done": "Γινώμενος", + "editQuickKeys": "Επεξεργασία γρήγορων πλήκτρων", + "end": "Τέλος", + "esc": "Esc", + "home": "Σπίτι", + "pageDown": "PgDn", + "pageUp": "PgUp", + "paste": "Πάστα", + "quickKeyPlaceholder": "π.χ. sudo ", + "quickKeysDesc": "Πατήστε × για κατάργηση. Υποστηρίζει έως και 8 χαρακτήρες.", + "quickKeysTitle": "Γρήγορα πλήκτρα", + "removeQuickKey": "Αφαιρώ", + "resetDefaults": "Επαναφορά στις προεπιλογές", + "shift": "Αλλαγή", + "tab": "Αυτί" + }, + "terminal": { + "autoTmuxEnableFailed": "Αποτυχία ενεργοποίησης του Auto-Tmux", + "autoTmuxEnabled": "Το Auto-Tmux είναι ενεργοποιημένο για {{host}}. Εφαρμόζεται από την επόμενη σύνδεση.", + "clipboardHttpWarning": "Δεν ήταν δυνατή η ανάγνωση του πρόχειρου. Ορισμένα προγράμματα περιήγησης περιορίζουν την πρόσβαση στο πρόχειρο εκτός HTTPS - δοκιμάστε Ctrl+V ή προβάλετε το Termix μέσω HTTPS.", + "closeTab": "Κοντά", + "connecting": "Συνδετικός...", + "connectionLost": "Η σύνδεση χάθηκε", + "connectionRejected": "Η σύνδεση απορρίφθηκε από τον διακομιστή. Ελέγξτε τον έλεγχο ταυτότητας και τη διαμόρφωση δικτύου.", + "connectionTimeout": "Λήξη χρονικού ορίου σύνδεσης", + "enableAutoTmuxAction": "Ενεργοποίηση Auto-Tmux", + "hostKeyRejected": "Η επαλήθευση του κλειδιού κεντρικού υπολογιστή SSH απορρίφθηκε. Η σύνδεση ακυρώθηκε.", + "linkDialogCopy": "Αντίγραφο", + "linkDialogOpen": "Ανοιχτό", + "linkDialogTitle": "Άνοιγμα συνδέσμου", + "maxReconnectAttemptsReached": "Επιτεύχθηκε ο μέγιστος αριθμός προσπαθειών επανασύνδεσης", + "noHostSelected": "Δεν έχει επιλεγεί κεντρικός υπολογιστής", + "opksshAuthFailed": "Ο έλεγχος ταυτότητας απέτυχε. Ελέγξτε τα διαπιστευτήριά σας και προσπαθήστε ξανά.", + "opksshTimeout": "Το χρονικό όριο ελέγχου ταυτότητας έληξε. Δοκιμάστε ξανά.", + "passwordPromptFillTitle": "Συμπληρώστε τον αποθηκευμένο κωδικό πρόσβασης σε αυτήν την ερώτηση;", + "reconnect": "Επανασύνδεση", + "reconnected": "Επανασύνδεση με επιτυχία", + "reconnecting": "Επανασύνδεση... ({{attempt}}/{{max}})", + "searchCaseSensitive": "Ταίριασμα υπόθεσης", + "searchClose": "Κλείσιμο (Escape)", + "searchNext": "Επόμενος αγώνας (Είσοδος)", + "searchNoResults": "Δεν υπάρχουν αποτελέσματα", + "searchPlaceholder": "Εύρημα", + "searchPrevious": "Προηγούμενος αγώνας (Shift+Enter)", + "searchRegex": "Χρήση κανονικής έκφρασης", + "searchResultCount": "{{index}} από {{count}}", + "searchWholeWord": "Ταίριασμα ολόκληρης λέξης", + "sessionExpiredNotice": "Η προηγούμενη συνεδρία σας στο {{host}} έληξε μετά από {{minutes}} λεπτά αποσύνδεσης. Το κέλυφος και οτιδήποτε εκτελείται σε αυτό έχουν εξαφανιστεί. Ενεργοποιήστε το Auto-Tmux σε αυτόν τον κεντρικό υπολογιστή για να διατηρήσετε ενεργές τις συνεδρίες.", + "sessionExpiredNoticeNoMinutes": "Η προηγούμενη συνεδρία σας στο {{host}} έληξε κατά την αποσύνδεση. Το κέλυφος και οτιδήποτε εκτελείται σε αυτό έχουν χαθεί. Ενεργοποιήστε το Auto-Tmux σε αυτόν τον κεντρικό υπολογιστή για να διατηρήσετε ενεργές τις συνεδρίες.", + "sessionTakenOver": "Η συνεδρία άνοιξε σε άλλη καρτέλα. Επανασύνδεση...", + "tmuxAttached": "Συνδεδεμένοι πελάτες", + "tmuxAttachedCount": "{{count}} επισυνάπτεται", + "tmuxCopyHint": "Προσαρμόστε την επιλογή και πατήστε Enter για αντιγραφή στο πρόχειρο", + "tmuxCreateNew": "Έναρξη νέας συνεδρίας", + "tmuxDetached": "Αποσπάστηκε από την περίοδο λειτουργίας tmux", + "tmuxLastActivity": "Τελευταία δραστηριότητα", + "tmuxSessionAttached": "συνεδρία tmux επισυνάπτεται: {{name}}", + "tmuxSessionCreated": "δημιουργήθηκε η συνεδρία tmux: {{name}}", + "tmuxSessionPickerDesc": "Βρέθηκαν υπάρχουσες συνεδρίες tmux σε αυτόν τον κεντρικό υπολογιστή. Επιλέξτε μία για να επανασυνδέσετε ή να δημιουργήσετε μια νέα συνεδρία.", + "tmuxSessionPickerTitle": "Συνεδρίες tmux", + "tmuxTimeDays": "{{count}}πριν από ημέρες", + "tmuxTimeHours": "{{count}}πριν από ώρες", + "tmuxTimeJustNow": "μόλις τώρα", + "tmuxTimeMinutes": "{{count}}πριν από λεπτά", + "tmuxUnavailable": "Το tmux δεν είναι εγκατεστημένο στον απομακρυσμένο κεντρικό υπολογιστή, επιστρέφοντας στο τυπικό κέλυφος", + "tmuxWindowCount": "παράθυρο {{count}}", + "tmuxWindows": "Παράθυρα", + "unknownError": "Παρουσιάστηκε άγνωστο σφάλμα", + "vaultAuthDescription": "Έχει ανοίξει ένα παράθυρο για να συνδεθείτε στο HashiCorp Vault. Ολοκληρώστε τη σύνδεση εκεί. Αυτή η σύνδεση θα συνεχιστεί αυτόματα.", + "vaultAuthFailed": "Ο έλεγχος ταυτότητας του vault απέτυχε. Δοκιμάστε ξανά.", + "vaultAuthTitle": "Απαιτείται σύνδεση στο Vault", + "vaultReopen": "Επανάνοιγμα παραθύρου σύνδεσης", + "websocketAbnormalClose": "Η σύνδεση έκλεισε απροσδόκητα. Αυτό μπορεί να οφείλεται σε πρόβλημα αντίστροφης μεσολάβησης ή ρύθμισης παραμέτρων SSL. Ελέγξτε τα αρχεία καταγραφής διακομιστή.", + "websocketError": "Σφάλμα σύνδεσης WebSocket" + }, + "terminalToolbar": { + "detachTmux": "Αποσύνδεση tmux", + "detachTmuxDescription": "Αποσύνδεση συνεδρίας tmux", + "dismissError": "Σφάλμα απόρριψης εικόνας", + "hideToolbar": "Απόκρυψη γραμμής εργαλείων", + "image": "Ενέργειες εικόνας (μόνο εικόνες)", + "imageActionFailed": "Η ενέργεια εικόνας απέτυχε", + "layout": "Λειτουργία εμφάνισης γραμμής εργαλείων", + "layoutExpanded": "Επεκτάθηκε με μετρήσεις", + "layoutIcon": "Μόνο εικονίδιο", + "layoutLabeled": "Εικονίδια και ετικέτες", + "moveToolbar": "Μετακίνηση γραμμής εργαλείων", + "moveToolbarHint": "Σύρετε για να αλλάξετε τη θέση της γραμμής εργαλείων", + "paste": "Επικόλληση εικόνας", + "pasteImage": "Επικόλληση εικόνας από το πρόχειρο (μόνο εικόνες)", + "retry": "Δοκιμάζω πάλι", + "showToolbar": "Ανάπτυξη γραμμής εργαλείων", + "upload": "Μεταφόρτωση εικόνας", + "uploadImage": "Μεταφορτώστε μια εικόνα στον κεντρικό υπολογιστή", + "uploadingImage": "Μεταφόρτωση..." + }, + "history": { + "noTerminalSelected": "Δεν έχει επιλεγεί τερματικό", + "noTerminalSelectedHint": "Ανοίξτε μια καρτέλα τερματικού SSH για να δείτε το ιστορικό εντολών του", + "searchPlaceholder": "Ιστορικό αναζήτησης...", + "clearAll": "Εκκαθάριση όλων", + "noHistoryEntries": "Δεν υπάρχουν καταχωρήσεις ιστορικού", + "trackingDisabled": "Η παρακολούθηση ιστορικού είναι απενεργοποιημένη" + }, + "touchInput": { + "enabled": "Ενεργοποίηση εισαγωγής αφής", + "enabledDesc": "Επιτρέψτε την κύλιση με ένα δάχτυλο σε τερματικά", + "momentumEnabled": "Ενεργοποίηση ορμής", + "momentumEnabledDesc": "Συνεχίστε σύντομα την κύλιση μετά από μια σάρωση", + "advanced": "Προχωρημένος", + "resetDefaults": "Επαναφορά στις προεπιλογές", + "dragThreshold": "Όριο οπισθέλκουσας", + "maxWheelDelta": "Μέγιστο δέλτα τροχού", + "momentumSampleWindow": "Παράθυρο δείγματος ορμής", + "releaseGracePeriod": "Περίοδος χάριτος αποδέσμευσης", + "minimumVelocity": "Ελάχιστη ταχύτητα", + "maximumVelocity": "Μέγιστη ταχύτητα", + "maximumDuration": "Μέγιστη διάρκεια", + "maximumTravel": "Μέγιστη διαδρομή", + "decayTime": "Χρόνος αποσύνθεσης", + "pixelsPerTick": "Εικονοστοιχεία ανά τικ", + "maximumFrameInterval": "Μέγιστο διάστημα καρέ", + "maximumTicksPerFrame": "Μέγιστος αριθμός τικ ανά καρέ" + }, + "imageStorage": { + "instanceId": "Αναγνωριστικό παρουσίας περιόδου λειτουργίας τερματικού", + "instanceIdDesc": "Αναγνωριστικό παρουσίας μίας από τις συνδεδεμένες καρτέλες τερματικού σας, που χρησιμοποιείται για τον έλεγχο της ορατότητας.", + "selectedModeUnavailable": "Δυσεύρετος", + "test": "Δοκιμή", + "testing": "Δοκιμές...", + "testFailed": "Αποτυχία δοκιμής χώρου αποθήκευσης εικόνων", + "testConnected": "Η συνεδρία τερματικού είναι συνδεδεμένη.", + "testNotConnected": "Δεν υπάρχει συνδεδεμένη συνεδρία τερματικού με αυτό το αναγνωριστικό παρουσίας.", + "testSelectedMode": "Μια μεταφόρτωση θα χρησιμοποιούσε: {{mode}}", + "testLocalVisible": "Ο τοπικός κατάλογος είναι ορατός από τον συνδεδεμένο κεντρικό υπολογιστή.", + "testLocalNotVisible": "Ο τοπικός κατάλογος ΔΕΝ είναι ορατός από τον συνδεδεμένο κεντρικό υπολογιστή." + }, + "settings": { + "admin": { + "group": { + "touch": "Εισαγωγή με άγγιγμα", + "images": "Αποθήκευση εικόνων" + }, + "sessionTimeoutMinutes": { + "label": "Διατήρηση Συνεδρίας Τερματικού", + "description": "Για πόσο χρονικό διάστημα διατηρείται ενεργό ένα αποσυνδεδεμένο τερματικό στον διακομιστή πριν κλείσει. Ελάχιστο 1 · Μέγιστο 1440 λεπτά. Για συνεδρίες που πρέπει να διαρκέσουν περισσότερο από αυτό, ενεργοποιήστε το Auto-Tmux στον κεντρικό υπολογιστή." + }, + "commandHistoryEnabled": { + "label": "Ιστορικό εντολών", + "description": "Να επιτρέπεται η καταγραφή ιστορικού εντολών. Όταν είναι απενεργοποιημένη, το ιστορικό δεν αποθηκεύεται ανεξάρτητα από τις ρυθμίσεις ανά κεντρικό υπολογιστή." + }, + "imageStorageMode": { + "label": "Λειτουργία αποθήκευσης", + "description": "Πού εγγράφονται επικολλημένες και μεταφορτωμένες εικόνες τερματικού. Οι αυτόματες επιλογές ανά σύνδεση, Τοπικές και Απομακρυσμένες (SFTP) έχουν διορθωθεί.", + "auto": "Αυτο", + "local": "Τοπικός", + "remoteSftp": "Απομακρυσμένο (SFTP)" + }, + "imageLocalDir": { + "label": "Τοπικός κατάλογος" + }, + "imageHostPath": { + "label": "Διαδρομή κεντρικού υπολογιστή", + "description": "Η προβολή από την πλευρά του κεντρικού υπολογιστή του τοπικού καταλόγου που λαμβάνουν οι πράκτορες τερματικού." + }, + "imageTtlMs": { + "label": "TTL (ms)" + }, + "imageMaxCount": { + "label": "Μέγιστος αριθμός εικόνων" + }, + "imageMaxBytes": { + "label": "Μέγιστος αριθμός byte αποθήκευσης" + } + }, + "host": { + "enableTerminalToolbar": { + "label": "Ενεργοποίηση γραμμής εργαλείων τερματικού", + "description": "Εμφάνιση μιας γραμμής εργαλείων γρήγορης πρόσβασης συνδεδεμένης στο ενεργό τερματικό για τα εργαλεία και τα ζωντανά στατιστικά αυτού του κεντρικού υπολογιστή" + }, + "enableCommandHistory": { + "label": "Ιστορικό εντολών", + "description": "Οι εντολές εγγραφής εκτελούνται σε αυτό το τερματικό για ιστορικό και αυτόματη συμπλήρωση" + }, + "enableTerminal": { + "label": "Ενεργοποίηση τερματικού" + } + } + }, + "palette": { + "localTerminal": "Τοπικό τερματικό" + } +} diff --git a/plugins/ssh-terminal/locales/translated/es_ES.json b/plugins/ssh-terminal/locales/translated/es_ES.json new file mode 100644 index 000000000..2dc0c7c51 --- /dev/null +++ b/plugins/ssh-terminal/locales/translated/es_ES.json @@ -0,0 +1,203 @@ +{ + "mobileKeyboard": { + "addQuickKey": "Añadir", + "arrowDown": "Flecha abajo", + "arrowLeft": "Flecha izquierda", + "arrowRight": "Flecha derecha", + "arrowUp": "Flecha arriba", + "backTab": "⇥", + "ctrl": "Ctrl", + "delete": "Supr", + "done": "Hecho", + "editQuickKeys": "Editar teclas rápidas", + "end": "Fin", + "esc": "Esc", + "home": "Inicio", + "pageDown": "Re Pág", + "pageUp": "Av Pág", + "paste": "Pasta", + "quickKeyPlaceholder": "p. ej. sudo ", + "quickKeysDesc": "Toca × para eliminar. Admite hasta 8 caracteres.", + "quickKeysTitle": "Teclas rápidas", + "removeQuickKey": "Eliminar", + "resetDefaults": "Restablecer", + "shift": "Mayús", + "tab": "Tab" + }, + "terminal": { + "autoTmuxEnableFailed": "No se pudo habilitar Auto-Tmux.", + "autoTmuxEnabled": "Auto-Tmux habilitado para {{host}}. Se aplica a partir de la siguiente conexión.", + "clipboardHttpWarning": "No se pudo leer el portapapeles. Algunos navegadores restringen el acceso al portapapeles fuera de HTTPS; intente usar Ctrl+V o ejecute Termix a través de HTTPS.", + "closeTab": "Cerrar", + "connecting": "Conectando...", + "connectionLost": "Conexión perdida", + "connectionRejected": "Conexión rechazada por el servidor. Revisa tu autenticación y configuración de red.", + "connectionTimeout": "Timeout de conexión", + "enableAutoTmuxAction": "Habilitar Auto-Tmux", + "hostKeyRejected": "Verificación de clave de host SSH rechazada. Conexión cancelada.", + "linkDialogCopy": "Copiar", + "linkDialogOpen": "Abrir", + "linkDialogTitle": "Abrir enlace", + "maxReconnectAttemptsReached": "Número máximo de intentos de reconexión alcanzado", + "noHostSelected": "Ningún host seleccionado", + "opksshAuthFailed": "La autenticación falló. Revisa tus credenciales e inténtalo de nuevo.", + "opksshTimeout": "La autenticación expiró. Inténtalo de nuevo.", + "passwordPromptFillTitle": "¿Rellenar este campo con la contraseña guardada?", + "reconnect": "Reconectar", + "reconnected": "Reconectado correctamente", + "reconnecting": "Reconectando... ({{attempt}}/{{max}})", + "searchCaseSensitive": "Caja de cerillas", + "searchClose": "Cerrar (Escape)", + "searchNext": "Siguiente partido (Entrar)", + "searchNoResults": "No hay resultados", + "searchPlaceholder": "Encontrar", + "searchPrevious": "Coincidencia anterior (Shift+Enter)", + "searchRegex": "Utilizar expresiones regulares", + "searchResultCount": "{{index}} de {{count}}", + "searchWholeWord": "Emparejar palabra completa", + "sessionExpiredNotice": "Tu sesión anterior en {{host}} expiró después de {{minutes}} minutos desconectada; la consola y todo lo que se ejecutaba en ella se han perdido. Habilita Auto-Tmux en este host para mantener las sesiones activas.", + "sessionExpiredNoticeNoMinutes": "Tu sesión anterior en {{host}} caducó mientras estabas desconectado; la consola y todo lo que se ejecutaba en ella se han perdido. Habilita Auto-Tmux en este host para mantener las sesiones activas.", + "sessionTakenOver": "La sesión se abrió en otra pestaña. Reconectando...", + "tmuxAttached": "Clientes adjuntos", + "tmuxAttachedCount": "{{count}} adjunto", + "tmuxCopyHint": "Ajuste la selección y pulse Intro para copiar al portapapeles", + "tmuxCreateNew": "Iniciar nueva sesión", + "tmuxDetached": "Desconectado de la sesión tmux", + "tmuxLastActivity": "Última actividad", + "tmuxSessionAttached": "Sesión tmux adjuntada: {{name}}", + "tmuxSessionCreated": "Sesión tmux creada: {{name}}", + "tmuxSessionPickerDesc": "Se encontraron sesiones tmux existentes en este host. Seleccione una para readjuntar o cree una nueva sesión.", + "tmuxSessionPickerTitle": "Sesiones tmux", + "tmuxTimeDays": "hace {{count}}d", + "tmuxTimeHours": "hace {{count}}h", + "tmuxTimeJustNow": "ahora mismo", + "tmuxTimeMinutes": "hace {{count}}m", + "tmuxUnavailable": "tmux no está instalado en el host remoto, recurriendo al intérprete de comandos estándar", + "tmuxWindowCount": "{{count}} ventana", + "tmuxWindows": "Ventanas", + "unknownError": "Se ha producido un error desconocido.", + "vaultAuthDescription": "Se ha abierto una ventana para iniciar sesión en HashiCorp Vault. Completa el inicio de sesión allí; esta conexión continuará automáticamente.", + "vaultAuthFailed": "La autenticación de Vault falló. Inténtalo de nuevo.", + "vaultAuthTitle": "Inicio de sesión en Vault requerido", + "vaultReopen": "Reabrir ventana de inicio de sesión", + "websocketAbnormalClose": "La conexión se cerró de forma inesperada. Esto puede deberse a un problema con el proxy inverso o la configuración SSL. Revisa los registros del servidor.", + "websocketError": "Error de conexión WebSocket" + }, + "terminalToolbar": { + "detachTmux": "Desvincular tmux", + "detachTmuxDescription": "Desvincular sesión de tmux", + "dismissError": "Descartar error de imagen", + "hideToolbar": "Ocultar barra de herramientas", + "image": "Acciones de imagen (solo imágenes)", + "imageActionFailed": "La acción de la imagen falló.", + "layout": "Modo de visualización de la barra de herramientas", + "layoutExpanded": "Ampliado con métricas", + "layoutIcon": "Solo icono", + "layoutLabeled": "Iconos y etiquetas", + "moveToolbar": "Mover barra de herramientas", + "moveToolbarHint": "Arrastra para cambiar la posición de la barra de herramientas.", + "paste": "Pegar imagen", + "pasteImage": "Pegar imagen desde el portapapeles (solo imágenes)", + "retry": "Rever", + "showToolbar": "Expandir barra de herramientas", + "upload": "Subir imagen", + "uploadImage": "Sube una imagen al servidor.", + "uploadingImage": "Subiendo..." + }, + "history": { + "noTerminalSelected": "Ningún terminal seleccionado", + "noTerminalSelectedHint": "Abra una pestaña de terminal SSH para ver su historial de comandos", + "searchPlaceholder": "Buscar en el historial...", + "clearAll": "Borrar todo", + "noHistoryEntries": "Sin entradas de historial", + "trackingDisabled": "El seguimiento del historial está desactivado" + }, + "touchInput": { + "enabled": "Habilitar la entrada táctil", + "enabledDesc": "Permitir el desplazamiento táctil con un solo dedo en terminales.", + "momentumEnabled": "Habilitar el impulso", + "momentumEnabledDesc": "Continúa desplazándote brevemente después de deslizar el dedo.", + "advanced": "Avanzado", + "resetDefaults": "Restablecer valores predeterminados", + "dragThreshold": "Umbral de arrastre", + "maxWheelDelta": "Delta máximo de rueda", + "momentumSampleWindow": "Ventana de muestreo de Momentum", + "releaseGracePeriod": "Período de gracia de liberación", + "minimumVelocity": "Velocidad mínima", + "maximumVelocity": "Velocidad máxima", + "maximumDuration": "Duración máxima", + "maximumTravel": "Viaje máximo", + "decayTime": "Tiempo de decaimiento", + "pixelsPerTick": "Píxeles por tick", + "maximumFrameInterval": "Intervalo máximo de fotogramas", + "maximumTicksPerFrame": "Número máximo de ticks por fotograma" + }, + "imageStorage": { + "instanceId": "ID de instancia de sesión de terminal", + "instanceIdDesc": "ID de instancia de una de las pestañas de terminal conectadas, utilizada para probar la visibilidad.", + "selectedModeUnavailable": "Indisponible", + "test": "Prueba", + "testing": "Pruebas...", + "testFailed": "No se pudo probar el almacenamiento de imágenes.", + "testConnected": "La sesión de terminal está conectada.", + "testNotConnected": "No existe ninguna sesión de terminal conectada con este ID de instancia.", + "testSelectedMode": "Una carga usaría: {{mode}}", + "testLocalVisible": "El directorio local es visible desde el host conectado.", + "testLocalNotVisible": "El directorio local NO es visible desde el host conectado." + }, + "settings": { + "admin": { + "group": { + "touch": "Entrada táctil", + "images": "Almacenamiento de imágenes" + }, + "sessionTimeoutMinutes": { + "label": "Persistencia de sesión terminal", + "description": "¿Cuánto tiempo permanece activa una terminal desconectada en el servidor antes de cerrarse? Mínimo 1 minuto · Máximo 1440 minutos. Para sesiones que deban durar más tiempo, active Auto-Tmux en el host." + }, + "commandHistoryEnabled": { + "label": "Historial de comandos", + "description": "Permitir el registro del historial de comandos. Si se desactiva, el historial no se guarda independientemente de la configuración por host." + }, + "imageStorageMode": { + "label": "Modo de almacenamiento", + "description": "Donde se escriben las imágenes de terminal pegadas y cargadas. La selección automática por conexión, local y remota (SFTP) es fija.", + "auto": "Auto", + "local": "Local", + "remoteSftp": "Remoto (SFTP)" + }, + "imageLocalDir": { + "label": "Directorio local" + }, + "imageHostPath": { + "label": "Ruta del host", + "description": "La vista del directorio local que reciben los agentes de terminal desde el lado del host." + }, + "imageTtlMs": { + "label": "TTL (ms)" + }, + "imageMaxCount": { + "label": "Número máximo de imágenes" + }, + "imageMaxBytes": { + "label": "Bytes de almacenamiento máximo" + } + }, + "host": { + "enableTerminalToolbar": { + "label": "Habilitar la barra de herramientas del terminal", + "description": "Mostrar una barra de herramientas de acceso rápido acoplada al terminal activo para las herramientas y estadísticas en tiempo real de este host." + }, + "enableCommandHistory": { + "label": "Historial de comandos", + "description": "Registrar los comandos ejecutados en este terminal para el historial y autocompletado" + }, + "enableTerminal": { + "label": "Activar terminal" + } + } + }, + "palette": { + "localTerminal": "Terminal local" + } +} diff --git a/plugins/ssh-terminal/locales/translated/fi_FI.json b/plugins/ssh-terminal/locales/translated/fi_FI.json new file mode 100644 index 000000000..8b4b014f1 --- /dev/null +++ b/plugins/ssh-terminal/locales/translated/fi_FI.json @@ -0,0 +1,203 @@ +{ + "mobileKeyboard": { + "addQuickKey": "Lisätä", + "arrowDown": "Nuoli alas", + "arrowLeft": "Nuoli vasemmalle", + "arrowRight": "Nuoli oikealle", + "arrowUp": "Nuoli ylös", + "backTab": "⇥", + "ctrl": "Ctrl", + "delete": "Poista", + "done": "Tehty", + "editQuickKeys": "Muokkaa pikanäppäimiä", + "end": "Loppu", + "esc": "Esc-näppäin", + "home": "Kotiin", + "pageDown": "PgDn", + "pageUp": "Sivu ylös", + "paste": "Liitä", + "quickKeyPlaceholder": "esim. sudo ", + "quickKeysDesc": "Poista napauttamalla ×. Tukee enintään 8 merkkiä.", + "quickKeysTitle": "Pikanäppäimet", + "removeQuickKey": "Poistaa", + "resetDefaults": "Palauta oletusasetukset", + "shift": "Siirtää", + "tab": "Välilehti" + }, + "terminal": { + "autoTmuxEnableFailed": "Automaattisen Tmuxin käyttöönotto epäonnistui", + "autoTmuxEnabled": "Auto-Tmux käytössä kohteelle {{host}}. Se on voimassa seuraavasta yhteydestä alkaen.", + "clipboardHttpWarning": "Leikepöydän lukeminen epäonnistui. Jotkin selaimet rajoittavat leikepöydän käyttöä HTTPS-yhteyden ulkopuolella – kokeile Ctrl+V-näppäinyhdistelmää tai käytä Termixiä HTTPS-yhteyden kautta.", + "closeTab": "Lähellä", + "connecting": "Yhdistetään...", + "connectionLost": "Yhteys katkesi", + "connectionRejected": "Palvelin hylkäsi yhteyden. Tarkista todennus ja verkkoasetukset.", + "connectionTimeout": "Yhteyden aikakatkaisu", + "enableAutoTmuxAction": "Ota automaattinen Tmux käyttöön", + "hostKeyRejected": "SSH-isännän avaimen vahvistus hylättiin. Yhteys katkaistu.", + "linkDialogCopy": "Kopioida", + "linkDialogOpen": "Avata", + "linkDialogTitle": "Avaa linkki", + "maxReconnectAttemptsReached": "Yhteyden muodostamisen uudelleenyritysten enimmäismäärä saavutettu", + "noHostSelected": "Ei valittua isäntää", + "opksshAuthFailed": "Todennus epäonnistui. Tarkista tunnistetietosi ja yritä uudelleen.", + "opksshTimeout": "Todennus aikakatkaistiin. Yritä uudelleen.", + "passwordPromptFillTitle": "Täytä tallennettu salasana tähän kehotteeseen?", + "reconnect": "Yhdistä uudelleen", + "reconnected": "Yhdistäminen uudelleen onnistui", + "reconnecting": "Yhdistetään uudelleen... ({{attempt}}/{{max}})", + "searchCaseSensitive": "Ottelukotelo", + "searchClose": "Sulje (Esc)", + "searchNext": "Seuraava osuma (Enter)", + "searchNoResults": "Ei tuloksia", + "searchPlaceholder": "Löytää", + "searchPrevious": "Edellinen osuma (Vaihto+Enter)", + "searchRegex": "Käytä säännöllistä lauseketta", + "searchResultCount": "{{index}} / {{count}}", + "searchWholeWord": "Koko sanan täsmäys", + "sessionExpiredNotice": "Edellinen istuntosi kohdassa {{host}} päättyi {{minutes}} minuutin katkoksen jälkeen; komentotulkki ja kaikki siinä käynnissä oleva on poistettu. Ota automaattinen Tmux käyttöön tällä koneella pitääksesi istunnot toiminnassa.", + "sessionExpiredNoticeNoMinutes": "Edellinen istuntosi koneella {{host}} päättyi yhteyden katkettua; komentotulkki ja kaikki siinä käynnissä oleva on poissa. Ota automaattinen Tmux käyttöön tällä koneella pitääksesi istunnot toiminnassa.", + "sessionTakenOver": "Istunto avattiin toisella välilehdellä. Yhdistetään uudelleen...", + "tmuxAttached": "Liitetyt asiakkaat", + "tmuxAttachedCount": "{{count}} liitteenä", + "tmuxCopyHint": "Säädä valintaa ja kopioi leikepöydälle painamalla Enter", + "tmuxCreateNew": "Aloita uusi istunto", + "tmuxDetached": "Irrotettu tmux-istunnosta", + "tmuxLastActivity": "Viimeisin aktiviteetti", + "tmuxSessionAttached": "tmux-istunto liitetty: {{name}}", + "tmuxSessionCreated": "tmux-istunto luotu: {{name}}", + "tmuxSessionPickerDesc": "Tältä isännältä löytyi olemassa olevia tmux-istuntoja. Valitse yksi liittääksesi sen uudelleen tai luodaksesi uuden istunnon.", + "tmuxSessionPickerTitle": "tmux-istunnot", + "tmuxTimeDays": "{{count}}d sitten", + "tmuxTimeHours": "{{count}}tuntia sitten", + "tmuxTimeJustNow": "juuri nyt", + "tmuxTimeMinutes": "{{count}}minuuttia sitten", + "tmuxUnavailable": "tmuxia ei ole asennettu etäkoneeseen, vaan käytetään vakiokuorta.", + "tmuxWindowCount": "{{count}} ikkuna", + "tmuxWindows": "Ikkunat", + "unknownError": "Tuntematon virhe tapahtui", + "vaultAuthDescription": "Ikkuna HashiCorp-holviin kirjautumista varten on avautunut. Kirjaudu sisään siinä. Yhteys jatkuu automaattisesti.", + "vaultAuthFailed": "Holvin todennus epäonnistui. Yritä uudelleen.", + "vaultAuthTitle": "Holviin kirjautuminen vaaditaan", + "vaultReopen": "Avaa kirjautumisikkuna uudelleen", + "websocketAbnormalClose": "Yhteys katkesi odottamatta. Tämä voi johtua käänteisestä välityspalvelimesta tai SSL-määritysongelmasta. Tarkista palvelimen lokit.", + "websocketError": "WebSocket-yhteysvirhe" + }, + "terminalToolbar": { + "detachTmux": "Irrota tmux", + "detachTmuxDescription": "Irrota tmux-istunto", + "dismissError": "Hylkää kuvavirhe", + "hideToolbar": "Piilota työkalupalkki", + "image": "Kuvatoiminnot (vain kuvat)", + "imageActionFailed": "Kuvatoiminto epäonnistui", + "layout": "Työkalurivin näyttötila", + "layoutExpanded": "Laajennettu mittareilla", + "layoutIcon": "Vain kuvake", + "layoutLabeled": "Kuvakkeet ja otsikot", + "moveToolbar": "Siirrä työkalupalkkia", + "moveToolbarHint": "Vedä työkalupalkin sijaintia", + "paste": "Liitä kuva", + "pasteImage": "Liitä kuva leikepöydältä (vain kuvat)", + "retry": "Yritä uudelleen", + "showToolbar": "Laajenna työkalupalkkia", + "upload": "Lataa kuva", + "uploadImage": "Lataa kuva isännöintipalveluun", + "uploadingImage": "Ladataan..." + }, + "history": { + "noTerminalSelected": "Ei valittua päätettä", + "noTerminalSelectedHint": "Avaa SSH-päätevälilehti nähdäksesi sen komentohistorian", + "searchPlaceholder": "Hakuhistoria...", + "clearAll": "Tyhjennä kaikki", + "noHistoryEntries": "Ei historiamerkintöjä", + "trackingDisabled": "Historian seuranta on poistettu käytöstä" + }, + "touchInput": { + "enabled": "Ota kosketussyöttö käyttöön", + "enabledDesc": "Salli yhden sormen kosketusvieritys päätteissä", + "momentumEnabled": "Ota vauhti käyttöön", + "momentumEnabledDesc": "Jatka vierittämistä lyhyesti pyyhkäisyn jälkeen", + "advanced": "Edistynyt", + "resetDefaults": "Palauta oletusasetukset", + "dragThreshold": "Vetokynnys", + "maxWheelDelta": "Suurin pyörän delta", + "momentumSampleWindow": "Momentumin näyteikkuna", + "releaseGracePeriod": "Vapautuksen lisäaika", + "minimumVelocity": "Miniminopeus", + "maximumVelocity": "Suurin nopeus", + "maximumDuration": "Enimmäiskesto", + "maximumTravel": "Maksimaalinen liikerata", + "decayTime": "Hajoamisaika", + "pixelsPerTick": "Pikseleitä rastia kohden", + "maximumFrameInterval": "Suurin kehysväli", + "maximumTicksPerFrame": "Maksimi tikit kehystä kohden" + }, + "imageStorage": { + "instanceId": "Pääteistunnon instanssin tunnus", + "instanceIdDesc": "Yhdistetyn päätevälilehden instanssi-ID, jota käytetään näkyvyyden testaamiseen.", + "selectedModeUnavailable": "Ei saatavilla", + "test": "Testata", + "testing": "Testaus...", + "testFailed": "Kuvan tallennuksen testaus epäonnistui", + "testConnected": "Pääteistunto on yhdistetty.", + "testNotConnected": "Tällä instanssi-ID:llä ei ole yhdistettyä pääteistuntoa.", + "testSelectedMode": "Lataus käyttäisi seuraavaa: {{mode}}", + "testLocalVisible": "Paikallinen hakemisto on näkyvissä yhdistetyssä isännässä.", + "testLocalNotVisible": "Paikallinen hakemisto EI ole näkyvissä yhdistetyssä isännässä." + }, + "settings": { + "admin": { + "group": { + "touch": "Kosketussyöttö", + "images": "Kuvien tallennus" + }, + "sessionTimeoutMinutes": { + "label": "Pääteistunnon pysyvyys", + "description": "Kuinka kauan irrotettua päätelaitetta pidetään toiminnassa palvelimella ennen sen sulkemista. Min 1 · Max 1440 minuuttia. Jos istunnon on kestettävä tätä pidempään, ota automaattinen Tmux käyttöön isännässä." + }, + "commandHistoryEnabled": { + "label": "Komentohistoria", + "description": "Salli komentohistorian tallennus. Kun tämä on poistettu käytöstä, historiaa ei tallenneta isäntäkohtaisista asetuksista riippumatta." + }, + "imageStorageMode": { + "label": "Tallennustila", + "description": "Mihin liitetyt ja ladatut päätekuvat kirjoitetaan. Automaattiset valinnat yhteyden, paikallisen ja etäyhteyden (SFTP) mukaan on korjattu.", + "auto": "Auto", + "local": "Paikallinen", + "remoteSftp": "Etäkäyttö (SFTP)" + }, + "imageLocalDir": { + "label": "Paikallinen hakemisto" + }, + "imageHostPath": { + "label": "Isäntäpolku", + "description": "Pääteagenttien vastaanottaman paikallisen hakemiston isäntäpuolen näkymä." + }, + "imageTtlMs": { + "label": "TTL (ms)" + }, + "imageMaxCount": { + "label": "Kuvien enimmäismäärä" + }, + "imageMaxBytes": { + "label": "Tallennustilan enimmäismäärä tavuja" + } + }, + "host": { + "enableTerminalToolbar": { + "label": "Ota käyttöön päätetyökalurivi", + "description": "Näytä aktiiviseen terminaaliin kiinnitetty pikakäyttöinen työkalupalkki tämän isännän työkaluille ja reaaliaikaisille tilastoille" + }, + "enableCommandHistory": { + "label": "Komentohistoria", + "description": "Tallenna tässä terminaalissa suoritettavat komennot historiaa ja automaattista täydennystä varten" + }, + "enableTerminal": { + "label": "Ota terminaali käyttöön" + } + } + }, + "palette": { + "localTerminal": "Paikallinen terminaali" + } +} diff --git a/plugins/ssh-terminal/locales/translated/fr_FR.json b/plugins/ssh-terminal/locales/translated/fr_FR.json new file mode 100644 index 000000000..86ce76cb7 --- /dev/null +++ b/plugins/ssh-terminal/locales/translated/fr_FR.json @@ -0,0 +1,203 @@ +{ + "mobileKeyboard": { + "addQuickKey": "Ajouter", + "arrowDown": "Flèche bas", + "arrowLeft": "Flèche gauche", + "arrowRight": "Flèche droite", + "arrowUp": "Flèche haut", + "backTab": "⇥", + "ctrl": "Ctrl", + "delete": "Suppr", + "done": "Terminé", + "editQuickKeys": "Modifier les touches rapides", + "end": "Fin", + "esc": "Échap", + "home": "Début", + "pageDown": "PgDn", + "pageUp": "PgUp", + "paste": "Coller", + "quickKeyPlaceholder": "ex. sudo ", + "quickKeysDesc": "Appuyez sur × pour supprimer. Prend en charge jusqu’à 8 caractères.", + "quickKeysTitle": "Touches rapides", + "removeQuickKey": "Supprimer", + "resetDefaults": "Rétablir les paramètres par défaut", + "shift": "Maj", + "tab": "Tab" + }, + "terminal": { + "autoTmuxEnableFailed": "Échec de l'activation d'Auto-Tmux", + "autoTmuxEnabled": "Auto-Tmux activé pour {{host}}. Cela s'appliquera à partir de la prochaine connexion.", + "clipboardHttpWarning": "Impossible de lire le contenu du presse-papiers. Certains navigateurs restreignent l'accès au presse-papiers en dehors du protocole HTTPS ; essayez plutôt Ctrl+V ou utilisez Termix via HTTPS.", + "closeTab": "Fermer", + "connecting": "Connexion en cours...", + "connectionLost": "Connexion perdue", + "connectionRejected": "Connexion rejetée par le serveur. Veuillez vérifier votre authentification et votre configuration réseau.", + "connectionTimeout": "Délai de connexion dépassé", + "enableAutoTmuxAction": "Activer Auto-Tmux", + "hostKeyRejected": "Vérification de la clé de l'hôte SSH rejetée. Connexion annulée.", + "linkDialogCopy": "Copier", + "linkDialogOpen": "Ouvrir", + "linkDialogTitle": "Ouvrir le lien", + "maxReconnectAttemptsReached": "Nombre maximal de tentatives de reconnexion atteint", + "noHostSelected": "Aucun hôte sélectionné", + "opksshAuthFailed": "Échec de l'authentification. Veuillez vérifier vos identifiants et réessayer.", + "opksshTimeout": "L'authentification a expiré. Veuillez réessayer.", + "passwordPromptFillTitle": "Remplir ce champ avec le mot de passe enregistré ?", + "reconnect": "Reconnecter", + "reconnected": "Reconnexion réussie", + "reconnecting": "Reconnexion... ({{attempt}}/{{max}})", + "searchCaseSensitive": "Boîte d'allumettes", + "searchClose": "Fermer (Échap)", + "searchNext": "Prochain match (Entrée)", + "searchNoResults": "Aucun résultat", + "searchPlaceholder": "Trouver", + "searchPrevious": "Match précédent (Maj+Entrée)", + "searchRegex": "Utilisez une expression régulière", + "searchResultCount": "{{index}} de {{count}}", + "searchWholeWord": "Associer le mot entier", + "sessionExpiredNotice": "Votre session précédente sur {{host}} a expiré après {{minutes}} minutes de déconnexion ; le shell et tous les processus en cours d'exécution ont été supprimés. Activez Auto-Tmux sur cet hôte pour maintenir les sessions actives.", + "sessionExpiredNoticeNoMinutes": "Votre session précédente sur {{host}} a expiré pendant la déconnexion ; le shell et tous les processus en cours d'exécution ont été supprimés. Activez Auto-Tmux sur cet hôte pour maintenir les sessions actives.", + "sessionTakenOver": "La session a été ouverte dans un autre onglet. Reconnexion en cours...", + "tmuxAttached": "Clients attachés", + "tmuxAttachedCount": "{{count}} attaché(s)", + "tmuxCopyHint": "Ajustez la sélection et appuyez sur Entrée pour copier dans le presse-papiers", + "tmuxCreateNew": "Créer une nouvelle session", + "tmuxDetached": "Détaché de la session tmux", + "tmuxLastActivity": "Dernière activité", + "tmuxSessionAttached": "Session tmux attachée : {{name}}", + "tmuxSessionCreated": "Session tmux créée : {{name}}", + "tmuxSessionPickerDesc": "Des sessions tmux existantes ont été trouvées sur cet hôte. Sélectionnez-en une pour vous y rattacher ou créez une nouvelle session.", + "tmuxSessionPickerTitle": "Sessions tmux", + "tmuxTimeDays": "il y a {{count}} j", + "tmuxTimeHours": "il y a {{count}} h", + "tmuxTimeJustNow": "à l'instant", + "tmuxTimeMinutes": "il y a {{count}} min", + "tmuxUnavailable": "tmux n'est pas installé sur l'hôte distant, retour au shell standard", + "tmuxWindowCount": "{{count}} fenêtre(s)", + "tmuxWindows": "Fenêtres", + "unknownError": "Une erreur inconnue s'est produite", + "vaultAuthDescription": "Une fenêtre s'est ouverte pour vous connecter à HashiCorp Vault. Terminez la connexion dans cette fenêtre ; cette connexion continuera automatiquement.", + "vaultAuthFailed": "Échec de l'authentification Vault. Veuillez réessayer.", + "vaultAuthTitle": "Connexion à Vault requise", + "vaultReopen": "Rouvrir la fenêtre de connexion", + "websocketAbnormalClose": "Connexion fermée de manière inattendue. Cela peut être dû à un problème de proxy inverse ou de configuration SSL. Veuillez vérifier les journaux du serveur.", + "websocketError": "Erreur de connexion WebSocket" + }, + "terminalToolbar": { + "detachTmux": "Détacher tmux", + "detachTmuxDescription": "Détacher la session tmux", + "dismissError": "Erreur d'image", + "hideToolbar": "Masquer la barre d'outils", + "image": "Actions sur les images (images uniquement)", + "imageActionFailed": "L'action sur l'image a échoué", + "layout": "mode d'affichage de la barre d'outils", + "layoutExpanded": "Développé avec des indicateurs", + "layoutIcon": "Icône uniquement", + "layoutLabeled": "Icônes et étiquettes", + "moveToolbar": "Déplacer la barre d'outils", + "moveToolbarHint": "Faites glisser pour repositionner la barre d'outils", + "paste": "Coller l'image", + "pasteImage": "Coller une image depuis le presse-papiers (images uniquement)", + "retry": "Réessayer", + "showToolbar": "Barre d'outils Développer", + "upload": "Télécharger une image", + "uploadImage": "Téléversez une image sur l'hôte", + "uploadingImage": "Chargement en cours..." + }, + "history": { + "noTerminalSelected": "Aucun terminal sélectionné", + "noTerminalSelectedHint": "Ouvrez un onglet de terminal SSH pour voir son historique de commandes", + "searchPlaceholder": "Rechercher dans l'historique...", + "clearAll": "Tout effacer", + "noHistoryEntries": "Aucune entrée d'historique", + "trackingDisabled": "Le suivi de l'historique est désactivé" + }, + "touchInput": { + "enabled": "Activer l'entrée tactile", + "enabledDesc": "Autoriser le défilement tactile à un doigt dans les terminaux", + "momentumEnabled": "Activer l'élan", + "momentumEnabledDesc": "Continuez à faire défiler brièvement après un glissement.", + "advanced": "Avancé", + "resetDefaults": "Réinitialiser les paramètres par défaut", + "dragThreshold": "Seuil de glissement", + "maxWheelDelta": "Delta de roue maximal", + "momentumSampleWindow": "fenêtre d'échantillonnage de l'impulsion", + "releaseGracePeriod": "période de grâce pour la libération", + "minimumVelocity": "Vitesse minimale", + "maximumVelocity": "Vitesse maximale", + "maximumDuration": "Durée maximale", + "maximumTravel": "Voyage maximal", + "decayTime": "Temps de dégradation", + "pixelsPerTick": "Pixels par tick", + "maximumFrameInterval": "Intervalle d'images maximal", + "maximumTicksPerFrame": "Nombre maximal de ticks par image" + }, + "imageStorage": { + "instanceId": "ID d'instance de session du terminal", + "instanceIdDesc": "ID d'instance de l'un de vos onglets de terminal connectés, utilisé pour tester la visibilité.", + "selectedModeUnavailable": "Indisponible", + "test": "Test", + "testing": "Essai...", + "testFailed": "Échec du test du stockage d'images", + "testConnected": "La session terminal est connectée.", + "testNotConnected": "Aucune session de terminal connectée avec cet ID d'instance.", + "testSelectedMode": "Un téléchargement utiliserait : {{mode}}", + "testLocalVisible": "Le répertoire local est visible depuis l'hôte connecté.", + "testLocalNotVisible": "Le répertoire local n'est PAS visible depuis l'hôte connecté." + }, + "settings": { + "admin": { + "group": { + "touch": "Entrée tactile", + "images": "Stockage d'images" + }, + "sessionTimeoutMinutes": { + "label": "Persistance de session terminale", + "description": "Durée de vie d'un terminal déconnecté sur le serveur avant sa fermeture. Minimum : 1 minute ; maximum : 1 440 minutes. Pour les sessions qui doivent durer plus longtemps, activez Auto-Tmux sur l'hôte." + }, + "commandHistoryEnabled": { + "label": "Historique des commandes", + "description": "Autoriser l'enregistrement de l'historique des commandes. Lorsque désactivé, l'historique n'est pas sauvegardé, indépendamment des paramètres par hôte." + }, + "imageStorageMode": { + "label": "Mode de stockage", + "description": "Emplacement où sont écrites les images du terminal collées et téléchargées. La sélection automatique par connexion (locale et distante [SFTP]) est fixe.", + "auto": "Auto", + "local": "Locale", + "remoteSftp": "Accès distant (SFTP)" + }, + "imageLocalDir": { + "label": "Répertoire local" + }, + "imageHostPath": { + "label": "Chemin hôte", + "description": "Vue côté hôte du répertoire local reçu par les agents terminaux." + }, + "imageTtlMs": { + "label": "TTL (ms)" + }, + "imageMaxCount": { + "label": "Nombre maximal d'images" + }, + "imageMaxBytes": { + "label": "Capacité de stockage maximale" + } + }, + "host": { + "enableTerminalToolbar": { + "label": "Activer la barre d'outils du terminal", + "description": "Afficher une barre d'outils d'accès rapide ancrée au terminal actif pour les outils et les statistiques en direct de cet hôte." + }, + "enableCommandHistory": { + "label": "Historique des commandes", + "description": "Enregistrer les commandes exécutées dans ce terminal pour l'historique et l'autocomplétion" + }, + "enableTerminal": { + "label": "Activer le terminal" + } + } + }, + "palette": { + "localTerminal": "Terminal local" + } +} diff --git a/plugins/ssh-terminal/locales/translated/he_IL.json b/plugins/ssh-terminal/locales/translated/he_IL.json new file mode 100644 index 000000000..c834126a3 --- /dev/null +++ b/plugins/ssh-terminal/locales/translated/he_IL.json @@ -0,0 +1,203 @@ +{ + "mobileKeyboard": { + "addQuickKey": "לְהוֹסִיף", + "arrowDown": "חץ למטה", + "arrowLeft": "חץ שמאלה", + "arrowRight": "חץ ימינה", + "arrowUp": "חץ למעלה", + "backTab": "⇥", + "ctrl": "קונטרול", + "delete": "דל", + "done": "נַעֲשָׂה", + "editQuickKeys": "עריכת מקשי מהירים", + "end": "סוֹף", + "esc": "Esc", + "home": "בַּיִת", + "pageDown": "PgDn", + "pageUp": "עמוד למעלה", + "paste": "לְהַדבִּיק", + "quickKeyPlaceholder": "למשל סודו ", + "quickKeysDesc": "הקש על × כדי להסיר. תומך בעד 8 תווים.", + "quickKeysTitle": "מקשי מהירים", + "removeQuickKey": "לְהַסִיר", + "resetDefaults": "איפוס להגדרות ברירת מחדל", + "shift": "מִשׁמֶרֶת", + "tab": "טאב" + }, + "terminal": { + "autoTmuxEnableFailed": "נכשל בהפעלת Auto-Tmux", + "autoTmuxEnabled": "Auto-Tmux מופעל עבור {{host}}. זה חל מהחיבור הבא.", + "clipboardHttpWarning": "לא ניתן היה לקרוא את הלוח. חלק מהדפדפנים מגבילים את הגישה ללוח מחוץ ל-HTTPS - נסה Ctrl+V במקום זאת, או שלח את Termix דרך HTTPS.", + "closeTab": "לִסְגוֹר", + "connecting": "מְקַשֵׁר...", + "connectionLost": "החיבור אבד", + "connectionRejected": "החיבור נדחה על ידי השרת. אנא בדוק את האימות ותצורת הרשת שלך.", + "connectionTimeout": "זמן קצוב לחיבור", + "enableAutoTmuxAction": "הפעל את Auto-Tmux", + "hostKeyRejected": "אימות מפתח מארח SSH נדחה. החיבור בוטל.", + "linkDialogCopy": "לְהַעְתִיק", + "linkDialogOpen": "לִפְתוֹחַ", + "linkDialogTitle": "פתח קישור", + "maxReconnectAttemptsReached": "הגעת למספר המקסימלי של ניסיונות חיבור מחדש", + "noHostSelected": "לא נבחר מארח", + "opksshAuthFailed": "האימות נכשל. אנא בדוק את פרטי הגישה שלך ונסה שוב.", + "opksshTimeout": "הזמן שהוקצב לאימות הסתיים. אנא נסה שוב.", + "passwordPromptFillTitle": "למלא את הסיסמה השמורה בשורת הפקודה הזו?", + "reconnect": "התחבר מחדש", + "reconnected": "התחבר מחדש בהצלחה", + "reconnecting": "מתחבר מחדש... ({{attempt}}/{{max}})", + "searchCaseSensitive": "מארז התאמה", + "searchClose": "סגור (Escape)", + "searchNext": "המשחק הבא (Enter)", + "searchNoResults": "אין תוצאות", + "searchPlaceholder": "לִמצוֹא", + "searchPrevious": "התאמה קודמת (Shift+Enter)", + "searchRegex": "השתמש בביטוי רגולרי", + "searchResultCount": "{{index}} מתוך {{count}}", + "searchWholeWord": "התאמת מילה שלמה", + "sessionExpiredNotice": "הסשן הקודם שלך ב- {{host}} פג תוקפו לאחר {{minutes}} דקות של ניתוק; המעטפת וכל מה שפועל בה נעלם. הפעל את Auto-Tmux במארח זה כדי לשמור על הסשנים פעילים.", + "sessionExpiredNoticeNoMinutes": "הסשן הקודם שלך ב- {{host}} פג תוקפו בזמן ניתוק; המעטפת וכל מה שפועל בה נעלם. הפעל את Auto-Tmux במארח זה כדי לשמור על הסשנים פעילים.", + "sessionTakenOver": "הסשן נפתח בכרטיסייה אחרת. מתחבר מחדש...", + "tmuxAttached": "לקוחות מצורפים", + "tmuxAttachedCount": "{{count}} מצורף", + "tmuxCopyHint": "התאם את הבחירה ולחץ על Enter כדי להעתיק ללוח", + "tmuxCreateNew": "התחל סשן חדש", + "tmuxDetached": "מנותק מסשן tmux", + "tmuxLastActivity": "פעילות אחרונה", + "tmuxSessionAttached": "סשן tmux מצורף: {{name}}", + "tmuxSessionCreated": "נוצרה סשן tmux: {{name}}", + "tmuxSessionPickerDesc": "נמצאו הפעלות tmux קיימות במארח זה. בחר אחת לחיבור מחדש או צור הפעלות חדשה.", + "tmuxSessionPickerTitle": "מפגשי tmux", + "tmuxTimeDays": "{{count}}d ago", + "tmuxTimeHours": "{{count}}h ago", + "tmuxTimeJustNow": "זֶה עַתָה", + "tmuxTimeMinutes": "לפני {{count}}דקות", + "tmuxUnavailable": "tmux אינו מותקן על המארח המרוחק, חוזר למעטפת סטנדרטית", + "tmuxWindowCount": "חלון {{count}}", + "tmuxWindows": "חלונות", + "unknownError": "אירעה שגיאה לא ידועה", + "vaultAuthDescription": "נפתח חלון לכניסה לכספת HashiCorp. השלם את תהליך הכניסה שם; חיבור זה ימשיך אוטומטית.", + "vaultAuthFailed": "אימות הכספת נכשל. אנא נסה שוב.", + "vaultAuthTitle": "נדרשת כניסה לכספת", + "vaultReopen": "פתח מחדש את חלון הכניסה", + "websocketAbnormalClose": "החיבור נסגר באופן בלתי צפוי. ייתכן שהדבר נובע מבעיית פרוקסי הפוך או מבעיית תצורת SSL. אנא בדוק את יומני השרת.", + "websocketError": "שגיאת חיבור WebSocket" + }, + "terminalToolbar": { + "detachTmux": "ניתוק tmux", + "detachTmuxDescription": "ניתוק סשן tmux", + "dismissError": "סגור שגיאת תמונה", + "hideToolbar": "הסתר את סרגל הכלים", + "image": "פעולות תמונה (תמונות בלבד)", + "imageActionFailed": "פעולת התמונה נכשלה", + "layout": "מצב תצוגה של סרגל הכלים", + "layoutExpanded": "מורחב עם מדדים", + "layoutIcon": "סמל בלבד", + "layoutLabeled": "סמלים ותוויות", + "moveToolbar": "הזזת סרגל הכלים", + "moveToolbarHint": "גרור כדי לשנות את מיקום סרגל הכלים", + "paste": "הדבקת תמונה", + "pasteImage": "הדבקת תמונה מהלוח (תמונות בלבד)", + "retry": "נסה שוב", + "showToolbar": "הרחב את סרגל הכלים", + "upload": "העלאת תמונה", + "uploadImage": "העלה תמונה למארח", + "uploadingImage": "מעלה..." + }, + "history": { + "noTerminalSelected": "לא נבחר טרמינל", + "noTerminalSelectedHint": "פתח כרטיסיית מסוף SSH כדי להציג את היסטוריית הפקודות שלה", + "searchPlaceholder": "היסטוריית חיפוש...", + "clearAll": "נקה הכל", + "noHistoryEntries": "אין רשומות היסטוריה", + "trackingDisabled": "מעקב אחר היסטוריה מושבת" + }, + "touchInput": { + "enabled": "הפעל קלט מגע", + "enabledDesc": "אפשר גלילה באמצעות מגע באצבע אחת במסופים", + "momentumEnabled": "לאפשר מומנטום", + "momentumEnabledDesc": "המשך גלילה קצרה לאחר החלקה", + "advanced": "מִתקַדֵם", + "resetDefaults": "איפוס להגדרות ברירת מחדל", + "dragThreshold": "סף גרירה", + "maxWheelDelta": "דלתא גלגל מקסימלית", + "momentumSampleWindow": "חלון דוגמת מומנטום", + "releaseGracePeriod": "תקופת חסד לשחרור", + "minimumVelocity": "מהירות מינימלית", + "maximumVelocity": "מהירות מקסימלית", + "maximumDuration": "משך זמן מקסימלי", + "maximumTravel": "נסיעה מקסימלית", + "decayTime": "זמן דעיכה", + "pixelsPerTick": "פיקסלים לכל טיק", + "maximumFrameInterval": "מרווח פריימים מקסימלי", + "maximumTicksPerFrame": "מספר מקסימלי של טיקים למסגרת" + }, + "imageStorage": { + "instanceId": "מזהה מופע של סשן מסוף", + "instanceIdDesc": "מזהה מופע של אחת מכרטיסיות הטרמינל המחוברות שלך, המשמש לבדיקת נראות.", + "selectedModeUnavailable": "לא זמין", + "test": "מִבְחָן", + "testing": "בּוֹחֵן...", + "testFailed": "נכשלה בדיקת אחסון התמונה", + "testConnected": "סשן הטרמינל מחובר.", + "testNotConnected": "אין סשן מסוף מחובר עם מזהה מופע זה.", + "testSelectedMode": "העלאה תשתמש ב: {{mode}}", + "testLocalVisible": "הספרייה המקומית גלויה מהמארח המחובר.", + "testLocalNotVisible": "הספרייה המקומית אינה גלויה מהמארח המחובר." + }, + "settings": { + "admin": { + "group": { + "touch": "קלט מגע", + "images": "אחסון תמונות" + }, + "sessionTimeoutMinutes": { + "label": "התמדה של סשן מסוף", + "description": "כמה זמן מסוף מנותק נשאר פעיל בשרת לפני שהוא נסגר. מינימום 1 · מקסימום 1440 דקות. עבור סשנים שחייבים לשרוד יותר מ-10 דקות, הפעל את Auto-Tmux במארח." + }, + "commandHistoryEnabled": { + "label": "היסטוריית פקודות", + "description": "אפשר רישום היסטוריית פקודות. כאשר האפשרות מושבתת, ההיסטוריה לא נשמרת ללא קשר להגדרות של כל מחשב מארח." + }, + "imageStorageMode": { + "label": "מצב אחסון", + "description": "היכן נכתבות תמונות טרמינל שהודבקו והועלו. בחירות אוטומטיות לפי חיבור, מקומי ומרוחק (SFTP) תוקנו.", + "auto": "אוטומטי", + "local": "מְקוֹמִי", + "remoteSftp": "מרחוק (SFTP)" + }, + "imageLocalDir": { + "label": "ספרייה מקומית" + }, + "imageHostPath": { + "label": "נתיב המארח", + "description": "תצוגת המארח של הספרייה המקומית שמקבלים סוכני הטרמינל." + }, + "imageTtlMs": { + "label": "TTL (אלפיות שנייה)" + }, + "imageMaxCount": { + "label": "ספירת תמונות מקסימלית" + }, + "imageMaxBytes": { + "label": "מספר בייטים מקסימלי לאחסון" + } + }, + "host": { + "enableTerminalToolbar": { + "label": "הפעלת סרגל הכלים של הטרמינל", + "description": "הצג סרגל כלים לגישה מהירה המעוגן למסוף הפעיל עבור הכלים והסטטיסטיקות החיות של מארח זה" + }, + "enableCommandHistory": { + "label": "היסטוריית פקודות", + "description": "פקודות הקלטה פועלות בטרמינל זה לצורך היסטוריה והשלמה אוטומטית" + }, + "enableTerminal": { + "label": "הפעלת טרמינל" + } + } + }, + "palette": { + "localTerminal": "מסוף מקומי" + } +} diff --git a/plugins/ssh-terminal/locales/translated/hi_IN.json b/plugins/ssh-terminal/locales/translated/hi_IN.json new file mode 100644 index 000000000..4b7d72673 --- /dev/null +++ b/plugins/ssh-terminal/locales/translated/hi_IN.json @@ -0,0 +1,203 @@ +{ + "mobileKeyboard": { + "addQuickKey": "जोड़ें", + "arrowDown": "नीचे तीर", + "arrowLeft": "बाएँ तीर", + "arrowRight": "दाएँ तीर", + "arrowUp": "ऊपर तीर", + "backTab": "⇥", + "ctrl": "Ctrl", + "delete": "Del", + "done": "पूर्ण", + "editQuickKeys": "त्वरित कुंजियाँ संपादित करें", + "end": "एंड", + "esc": "Esc", + "home": "होम", + "pageDown": "PgDn", + "pageUp": "PgUp", + "paste": "पेस्ट करें", + "quickKeyPlaceholder": "उदा. sudo ", + "quickKeysDesc": "हटाने के लिए × पर टैप करें। अधिकतम 8 वर्ण समर्थित।", + "quickKeysTitle": "त्वरित कुंजियाँ", + "removeQuickKey": "हटाएँ", + "resetDefaults": "डिफ़ॉल्ट पर रीसेट करें", + "shift": "Shift", + "tab": "Tab" + }, + "terminal": { + "autoTmuxEnableFailed": "ऑटो-टीमक्स को सक्षम करने में विफल", + "autoTmuxEnabled": "{{host}}के लिए ऑटो-Tmux सक्षम किया गया है। यह अगले कनेक्शन से लागू होगा।", + "clipboardHttpWarning": "क्लिपबोर्ड पढ़ा नहीं जा सका। कुछ ब्राउज़र HTTPS के बाहर क्लिपबोर्ड एक्सेस को प्रतिबंधित करते हैं - इसके बजाय Ctrl+V का उपयोग करके देखें, या Termix को HTTPS पर चलाएं।", + "closeTab": "बंद करें", + "connecting": "कनेक्ट हो रहा है...", + "connectionLost": "कनेक्शन खो गया", + "connectionRejected": "सर्वर द्वारा कनेक्शन अस्वीकृत। कृपया अपनी प्रमाणीकरण और नेटवर्क कॉन्फ़िगरेशन जाँचें।", + "connectionTimeout": "कनेक्शन टाइमआउट", + "enableAutoTmuxAction": "ऑटो-टीमक्स सक्षम करें", + "hostKeyRejected": "SSH होस्ट कुंजी सत्यापन अस्वीकृत। कनेक्शन रद्द किया गया।", + "linkDialogCopy": "कॉपी करें", + "linkDialogOpen": "खोलें", + "linkDialogTitle": "लिंक खोलें", + "maxReconnectAttemptsReached": "अधिकतम पुनः कनेक्ट प्रयास पूर्ण हो गए", + "noHostSelected": "कोई होस्ट चयनित नहीं", + "opksshAuthFailed": "प्रमाणीकरण विफल। कृपया अपने क्रेडेंशियल जाँचें और पुनः प्रयास करें।", + "opksshTimeout": "प्रमाणीकरण का समय समाप्त हो गया। कृपया पुनः प्रयास करें।", + "passwordPromptFillTitle": "क्या सहेजे गए पासवर्ड को इस प्रांप्ट में भरें?", + "reconnect": "पुनः कनेक्ट करें", + "reconnected": "पुनः कनेक्ट हुआ", + "reconnecting": "पुनः कनेक्ट हो रहा है... ({{attempt}}/{{max}})", + "searchCaseSensitive": "मामले मिलाएं", + "searchClose": "बंद करें (एस्केप)", + "searchNext": "अगला मैच (प्रवेश करें)", + "searchNoResults": "कोई परिणाम नहीं", + "searchPlaceholder": "खोजो", + "searchPrevious": "पिछला मैच (शिफ्ट+एंटर)", + "searchRegex": "नियमित अभिव्यक्ति का उपयोग करें", + "searchResultCount": "{{index}} का {{count}}", + "searchWholeWord": "पूरे शब्द का मिलान करें", + "sessionExpiredNotice": "{{host}} पर आपका पिछला सत्र {{minutes}} मिनट बाद समाप्त हो गया; शेल और उसमें चल रही हर चीज़ समाप्त हो गई है। सत्रों को सक्रिय रखने के लिए इस होस्ट पर ऑटो-ट्मक्स सक्षम करें।", + "sessionExpiredNoticeNoMinutes": "{{host}} पर आपका पिछला सत्र डिस्कनेक्ट होने के दौरान समाप्त हो गया; शेल और उसमें चल रही हर चीज़ नष्ट हो गई है। सत्रों को सक्रिय रखने के लिए इस होस्ट पर ऑटो-ट्मक्स सक्षम करें।", + "sessionTakenOver": "सत्र किसी अन्य टैब में खोला गया था। पुनः कनेक्ट हो रहा है...", + "tmuxAttached": "अटैच किए गए क्लाइंट", + "tmuxAttachedCount": "{{count}} अटैच किए गए", + "tmuxCopyHint": "चयन समायोजित करें और क्लिपबोर्ड पर कॉपी करने के लिए Enter दबाएं", + "tmuxCreateNew": "नया सत्र प्रारंभ करें", + "tmuxDetached": "tmux सत्र से डिटैच हो गया", + "tmuxLastActivity": "अंतिम गतिविधि", + "tmuxSessionAttached": "tmux सत्र अटैच किया गया: {{name}}", + "tmuxSessionCreated": "tmux सत्र बनाया गया: {{name}}", + "tmuxSessionPickerDesc": "इस होस्ट पर मौजूदा tmux सत्र पाए गए। पुनः अटैच करने के लिए एक का चयन करें या नया सत्र बनाएं।", + "tmuxSessionPickerTitle": "tmux सत्र", + "tmuxTimeDays": "{{count}} दिन पहले", + "tmuxTimeHours": "{{count}} घंटे पहले", + "tmuxTimeJustNow": "अभी-अभी", + "tmuxTimeMinutes": "{{count}} मिनट पहले", + "tmuxUnavailable": "tmux रिमोट होस्ट पर स्थापित नहीं है, मानक शेल पर वापस लौट रहा है", + "tmuxWindowCount": "{{count}} विंडो", + "tmuxWindows": "विंडो", + "unknownError": "अज्ञात त्रुटि हुई", + "vaultAuthDescription": "HashiCorp Vault में साइन इन करने के लिए एक विंडो खुल गई है। वहाँ साइन-इन पूरा करें; यह कनेक्शन स्वचालित रूप से जारी रहेगा।", + "vaultAuthFailed": "Vault प्रमाणीकरण विफल। कृपया पुनः प्रयास करें।", + "vaultAuthTitle": "Vault साइन-इन आवश्यक है", + "vaultReopen": "साइन-इन विंडो फिर से खोलें", + "websocketAbnormalClose": "कनेक्शन अप्रत्याशित रूप से बंद हो गया। यह रिवर्स प्रॉक्सी या SSL कॉन्फ़िगरेशन समस्या के कारण हो सकता है। कृपया सर्वर लॉग जाँचें।", + "websocketError": "WebSocket कनेक्शन त्रुटि" + }, + "terminalToolbar": { + "detachTmux": "tmux को अलग करें", + "detachTmuxDescription": "tmux सत्र को अलग करें", + "dismissError": "छवि त्रुटि को खारिज करें", + "hideToolbar": "उपकरण पट्टी छिपाओ", + "image": "छवि क्रियाएँ (केवल छवियाँ)", + "imageActionFailed": "छवि संबंधी कार्रवाई विफल रही", + "layout": "टूलबार डिस्प्ले मोड", + "layoutExpanded": "मैट्रिक्स के साथ विस्तारित", + "layoutIcon": "केवल आइकन", + "layoutLabeled": "आइकन और लेबल", + "moveToolbar": "टूलबार को स्थानांतरित करें", + "moveToolbarHint": "टूलबार की स्थिति बदलने के लिए उसे खींचें", + "paste": "छवि चिपकाएँ", + "pasteImage": "क्लिपबोर्ड से छवि पेस्ट करें (केवल छवियां)", + "retry": "पुन: प्रयास करें", + "showToolbar": "टूलबार का विस्तार करें", + "upload": "तस्विर अपलोड करना", + "uploadImage": "होस्ट पर एक छवि अपलोड करें", + "uploadingImage": "अपलोड हो रहा है..." + }, + "history": { + "noTerminalSelected": "कोई टर्मिनल चयनित नहीं", + "noTerminalSelectedHint": "कमांड इतिहास देखने के लिए SSH टर्मिनल टैब खोलें", + "searchPlaceholder": "इतिहास खोजें...", + "clearAll": "सभी साफ़ करें", + "noHistoryEntries": "कोई इतिहास प्रविष्टियाँ नहीं", + "trackingDisabled": "इतिहास ट्रैकिंग अक्षम है" + }, + "touchInput": { + "enabled": "टच इनपुट सक्षम करें", + "enabledDesc": "टर्मिनलों में एक उंगली से स्क्रॉल करने की सुविधा दें", + "momentumEnabled": "गति को सक्षम करें", + "momentumEnabledDesc": "स्वाइप करने के बाद थोड़ी देर स्क्रॉल करते रहें", + "advanced": "विकसित", + "resetDefaults": "डिफ़ॉल्ट पर पुनः सेट करें", + "dragThreshold": "ड्रैग थ्रेशहोल्ड", + "maxWheelDelta": "अधिकतम पहिया डेल्टा", + "momentumSampleWindow": "मोमेंटम सैंपल विंडो", + "releaseGracePeriod": "रिहाई की मोहलत", + "minimumVelocity": "न्यूनतम वेग", + "maximumVelocity": "अधिकतम वेग", + "maximumDuration": "अधिकतम अवधि", + "maximumTravel": "अधिकतम यात्रा", + "decayTime": "क्षय समय", + "pixelsPerTick": "पिक्सेल प्रति टिक", + "maximumFrameInterval": "अधिकतम फ्रेम अंतराल", + "maximumTicksPerFrame": "प्रति फ्रेम अधिकतम टिक" + }, + "imageStorage": { + "instanceId": "टर्मिनल सत्र इंस्टेंस आईडी", + "instanceIdDesc": "आपके कनेक्टेड टर्मिनल टैब में से किसी एक का इंस्टेंस आईडी, जिसका उपयोग दृश्यता का परीक्षण करने के लिए किया जाता है।", + "selectedModeUnavailable": "अनुपलब्ध", + "test": "परीक्षा", + "testing": "परीक्षण...", + "testFailed": "इमेज स्टोरेज का परीक्षण विफल रहा", + "testConnected": "टर्मिनल सत्र कनेक्ट हो गया है।", + "testNotConnected": "इस इंस्टेंस आईडी के साथ कोई कनेक्टेड टर्मिनल सेशन नहीं है।", + "testSelectedMode": "अपलोड करने के लिए निम्न का उपयोग किया जाएगा: {{mode}}", + "testLocalVisible": "कनेक्टेड होस्ट से लोकल डायरेक्टरी दिखाई देती है।", + "testLocalNotVisible": "स्थानीय डायरेक्टरी कनेक्टेड होस्ट से दिखाई नहीं देती है।" + }, + "settings": { + "admin": { + "group": { + "touch": "स्पर्श निवेश", + "images": "छवि संग्रहण" + }, + "sessionTimeoutMinutes": { + "label": "टर्मिनल सत्र दृढ़ता", + "description": "सर्वर पर डिस्कनेक्ट होने के बाद टर्मिनल को बंद करने से पहले कितनी देर तक चालू रखा जाता है। न्यूनतम 1 मिनट · अधिकतम 1440 मिनट। इससे अधिक समय तक चलने वाले सेशन के लिए, होस्ट पर ऑटो-ट्मक्स को सक्षम करें।" + }, + "commandHistoryEnabled": { + "label": "कमांड इतिहास", + "description": "कमांड इतिहास रिकॉर्डिंग की अनुमति दें। अक्षम होने पर, प्रति-होस्ट सेटिंग्स की परवाह किए बिना इतिहास सहेजा नहीं जाता है।" + }, + "imageStorageMode": { + "label": "भंडारण मोड", + "description": "जहां पेस्ट और अपलोड की गई टर्मिनल इमेज लिखी जाती हैं। कनेक्शन के आधार पर ऑटो पिकिंग की सुविधा उपलब्ध है, लोकल और रिमोट (SFTP) कनेक्शन दोनों ही तय हैं।", + "auto": "ऑटो", + "local": "स्थानीय", + "remoteSftp": "रिमोट (एसएफटीपी)" + }, + "imageLocalDir": { + "label": "स्थानीय निर्देशिका" + }, + "imageHostPath": { + "label": "होस्ट पथ", + "description": "टर्मिनल एजेंटों को प्राप्त होने वाली स्थानीय निर्देशिका का होस्ट-साइड दृश्य।" + }, + "imageTtlMs": { + "label": "टीटीएल (मिलीमीटर)" + }, + "imageMaxCount": { + "label": "अधिकतम छवि संख्या" + }, + "imageMaxBytes": { + "label": "अधिकतम संग्रहण बाइट्स" + } + }, + "host": { + "enableTerminalToolbar": { + "label": "टर्मिनल टूलबार को सक्षम करें", + "description": "इस होस्ट के टूल्स और लाइव स्टैट्स के लिए सक्रिय टर्मिनल से जुड़ा एक त्वरित-पहुँच टूलबार दिखाएँ।" + }, + "enableCommandHistory": { + "label": "कमांड इतिहास", + "description": "इतिहास और स्वतः पूर्ण के लिए इस टर्मिनल में चलाए गए कमांड रिकॉर्ड करें" + }, + "enableTerminal": { + "label": "टर्मिनल सक्षम करें" + } + } + }, + "palette": { + "localTerminal": "स्थानीय टर्मिनल" + } +} diff --git a/plugins/ssh-terminal/locales/translated/hu_HU.json b/plugins/ssh-terminal/locales/translated/hu_HU.json new file mode 100644 index 000000000..f3a9b86e2 --- /dev/null +++ b/plugins/ssh-terminal/locales/translated/hu_HU.json @@ -0,0 +1,203 @@ +{ + "mobileKeyboard": { + "addQuickKey": "Hozzáadás", + "arrowDown": "Lefelé mutató nyíl", + "arrowLeft": "Balra nyíl", + "arrowRight": "Jobbra nyíl", + "arrowUp": "Felfelé nyíl", + "backTab": "⇥", + "ctrl": "Ctrl", + "delete": "Del", + "done": "Kész", + "editQuickKeys": "Gyorsbillentyűk szerkesztése", + "end": "Vége", + "esc": "Esc", + "home": "Otthon", + "pageDown": "PgDn", + "pageUp": "PgUp", + "paste": "Paszta", + "quickKeyPlaceholder": "pl. sudo ", + "quickKeysDesc": "Koppintson a × jelre az eltávolításhoz. Legfeljebb 8 karaktert támogat.", + "quickKeysTitle": "Gyorsbillentyűk", + "removeQuickKey": "Eltávolítás", + "resetDefaults": "Alapértelmezett beállítások visszaállítása", + "shift": "Váltás", + "tab": "Fül" + }, + "terminal": { + "autoTmuxEnableFailed": "Nem sikerült engedélyezni az Auto-Tmux-ot", + "autoTmuxEnabled": "Auto-Tmux engedélyezve a {{host}}számára. A következő kapcsolattól érvényes.", + "clipboardHttpWarning": "Nem sikerült beolvasni a vágólapot. Néhány böngésző korlátozza a vágólap elérését HTTPS-en kívül - próbálja ki a Ctrl+V billentyűkombinációt, vagy HTTPS-en keresztül szolgáltassa ki a Termixet.", + "closeTab": "Közeli", + "connecting": "Kapcsolódás...", + "connectionLost": "Kapcsolat megszakadt", + "connectionRejected": "A szerver elutasította a kapcsolatot. Kérjük, ellenőrizze a hitelesítést és a hálózati konfigurációt.", + "connectionTimeout": "Kapcsolati időtúllépés", + "enableAutoTmuxAction": "Auto-Tmux engedélyezése", + "hostKeyRejected": "SSH host kulcs ellenőrzése elutasítva. Kapcsolat megszakítva.", + "linkDialogCopy": "Másolat", + "linkDialogOpen": "Nyitott", + "linkDialogTitle": "Link megnyitása", + "maxReconnectAttemptsReached": "Elérte az újracsatlakozási kísérletek maximális számát", + "noHostSelected": "Nincs kiválasztva gazdagép", + "opksshAuthFailed": "Sikertelen hitelesítés. Kérjük, ellenőrizze a hitelesítő adatait, és próbálja újra.", + "opksshTimeout": "A hitelesítés időtúllépést okozott. Kérjük, próbálja újra.", + "passwordPromptFillTitle": "Írja be a mentett jelszót ebbe a mezőbe?", + "reconnect": "Újracsatlakozás", + "reconnected": "Sikeresen újrakapcsolódott", + "reconnecting": "Újracsatlakozás... ({{attempt}}/{{max}})", + "searchCaseSensitive": "Gyufa tok", + "searchClose": "Bezárás (Escape)", + "searchNext": "Következő találat (Enter)", + "searchNoResults": "Nincs találat", + "searchPlaceholder": "Lelet", + "searchPrevious": "Előző találat (Shift+Enter)", + "searchRegex": "Reguláris kifejezés használata", + "searchResultCount": "{{index}} a {{count}}-ból", + "searchWholeWord": "Egész szó egyezése", + "sessionExpiredNotice": "Az előző munkameneted a következő helyen: {{host}} lejárt {{minutes}} perc lecsatlakozás után; a shell és minden, ami benne fut, eltűnt. Kapcsold be az Auto-Tmux-ot ezen a gépen a munkamenetek életben tartásához.", + "sessionExpiredNoticeNoMinutes": "Az előző munkameneted a {{host}} gépen lejárt, miközben le voltál választva; a shell és minden benne futó dolog elveszett. Kapcsold be az Auto-Tmuxot ezen a gépen a munkamenetek életben tartásához.", + "sessionTakenOver": "A munkamenet egy másik lapon nyílt meg. Újrakapcsolódás...", + "tmuxAttached": "Csatlakoztatott ügyfelek", + "tmuxAttachedCount": "{{count}} csatolva", + "tmuxCopyHint": "Módosítsa a kijelölést, és nyomja meg az Enter billentyűt a vágólapra másoláshoz", + "tmuxCreateNew": "Új munkamenet indítása", + "tmuxDetached": "Leválasztva a tmux munkamenetről", + "tmuxLastActivity": "Utolsó tevékenység", + "tmuxSessionAttached": "tmux munkamenet csatolva: {{name}}", + "tmuxSessionCreated": "tmux munkamenet létrehozva: {{name}}", + "tmuxSessionPickerDesc": "Meglévő tmux munkamenetek találhatók ezen a gazdagépen. Válasszon ki egyet az újracsatlakozáshoz, vagy hozzon létre egy új munkamenetet.", + "tmuxSessionPickerTitle": "tmux munkamenetek", + "tmuxTimeDays": "{{count}}nappal ezelőtt", + "tmuxTimeHours": "{{count}}órával ezelőtt", + "tmuxTimeJustNow": "éppen most", + "tmuxTimeMinutes": "{{count}}perccel ezelőtt", + "tmuxUnavailable": "A tmux nincs telepítve a távoli gépre, a standard shell-t használja.", + "tmuxWindowCount": "{{count}} ablak", + "tmuxWindows": "Ablakok", + "unknownError": "Ismeretlen hiba történt", + "vaultAuthDescription": "Megnyílt egy ablak a HashiCorp Vaultba való bejelentkezéshez. Fejezze be a bejelentkezést ott; a kapcsolat automatikusan folytatódik.", + "vaultAuthFailed": "A trezor hitelesítése sikertelen. Próbálja újra.", + "vaultAuthTitle": "Bejelentkezés szükséges a széfbe", + "vaultReopen": "Bejelentkezési ablak újranyitása", + "websocketAbnormalClose": "A kapcsolat váratlanul megszakadt. Ennek oka lehet fordított proxy vagy SSL konfigurációs probléma. Kérjük, ellenőrizze a szervernaplókat.", + "websocketError": "WebSocket csatlakozási hiba" + }, + "terminalToolbar": { + "detachTmux": "tmux leválasztása", + "detachTmuxDescription": "tmux munkamenet leválasztása", + "dismissError": "Képhiba elvetése", + "hideToolbar": "Eszköztár elrejtése", + "image": "Képműveletek (csak képek)", + "imageActionFailed": "Képművelet sikertelen", + "layout": "Eszköztár megjelenítési módja", + "layoutExpanded": "Metrikákkal bővítve", + "layoutIcon": "Csak ikon", + "layoutLabeled": "Ikonok és címkék", + "moveToolbar": "Eszköztár áthelyezése", + "moveToolbarHint": "Húzza az eszköztár áthelyezéséhez", + "paste": "Kép beillesztése", + "pasteImage": "Kép beillesztése a vágólapról (csak képek)", + "retry": "Újrapróbálkozás", + "showToolbar": "Eszköztár kibontása", + "upload": "Kép feltöltése", + "uploadImage": "Töltsön fel egy képet a tárhelyszolgáltatóra", + "uploadingImage": "Feltöltés..." + }, + "history": { + "noTerminalSelected": "Nincs kiválasztva terminál", + "noTerminalSelectedHint": "Nyisson meg egy SSH terminálfület a parancselőzmények megtekintéséhez", + "searchPlaceholder": "Keresési előzmények...", + "clearAll": "Összes törlése", + "noHistoryEntries": "Nincsenek előzménybejegyzések", + "trackingDisabled": "Az előzmények követése le van tiltva" + }, + "touchInput": { + "enabled": "Érintéses bevitel engedélyezése", + "enabledDesc": "Egyujjas görgetés engedélyezése a terminálokban", + "momentumEnabled": "Lendület engedélyezése", + "momentumEnabledDesc": "Görgessen tovább röviden egy húzás után", + "advanced": "Fejlett", + "resetDefaults": "Alapértelmezett beállítások visszaállítása", + "dragThreshold": "Húzási küszöb", + "maxWheelDelta": "Maximális kerékdelta", + "momentumSampleWindow": "Lendület mintavételi ablak", + "releaseGracePeriod": "Kiadási türelmi időszak", + "minimumVelocity": "Minimális sebesség", + "maximumVelocity": "Maximális sebesség", + "maximumDuration": "Maximális időtartam", + "maximumTravel": "Maximális mozgástartomány", + "decayTime": "Bomlási idő", + "pixelsPerTick": "Pixelek száma kullancsonként", + "maximumFrameInterval": "Maximális képkocka-intervallum", + "maximumTicksPerFrame": "Maximális tickek képkockánként" + }, + "imageStorage": { + "instanceId": "Terminál munkamenet példányazonosítója", + "instanceIdDesc": "Az egyik csatlakoztatott terminálfül példányazonosítója, amelyet a láthatóság teszteléséhez használunk.", + "selectedModeUnavailable": "Nem elérhető", + "test": "Teszt", + "testing": "Tesztelés...", + "testFailed": "Nem sikerült tesztelni a képtárolást", + "testConnected": "A terminál munkamenet csatlakozva van.", + "testNotConnected": "Nincs csatlakoztatott terminál munkamenet ezzel a példányazonosítóval.", + "testSelectedMode": "Egy feltöltés ezt használná: {{mode}}", + "testLocalVisible": "A helyi könyvtár látható a csatlakoztatott gépről.", + "testLocalNotVisible": "A helyi könyvtár NEM látható a csatlakoztatott gépről." + }, + "settings": { + "admin": { + "group": { + "touch": "Érintéses bevitel", + "images": "Képtárolás" + }, + "sessionTimeoutMinutes": { + "label": "Terminál munkamenet-megőrző", + "description": "Mennyi ideig marad életben egy leválasztott terminál a szerveren, mielőtt bezárásra kerül. Min. 1 · Max. 1440 perc. Azoknál a munkameneteknél, amelyeknek ezt túl kell élniük, engedélyezze az Auto-Tmux funkciót a gazdagépen." + }, + "commandHistoryEnabled": { + "label": "Parancsnoki előzmények", + "description": "Parancselőzmények rögzítésének engedélyezése. Ha le van tiltva, az előzmények nem kerülnek mentésre, függetlenül a gazdagép beállításaitól." + }, + "imageStorageMode": { + "label": "Tárolási mód", + "description": "A beillesztett és feltöltött terminálképek írási helye. Kijavítottuk az automatikus kiválasztást kapcsolat, helyi és távoli (SFTP) alapján.", + "auto": "Autó", + "local": "Helyi", + "remoteSftp": "Távoli (SFTP)" + }, + "imageLocalDir": { + "label": "Helyi címtár" + }, + "imageHostPath": { + "label": "Gazdagép elérési útja", + "description": "A terminálügynökök által fogadott helyi könyvtár hosztoldali nézete." + }, + "imageTtlMs": { + "label": "TTL (ms)" + }, + "imageMaxCount": { + "label": "Max. képszám" + }, + "imageMaxBytes": { + "label": "Max. tárhely byte-okban" + } + }, + "host": { + "enableTerminalToolbar": { + "label": "Terminál eszköztár engedélyezése", + "description": "Gyorselérési eszköztár megjelenítése az aktív terminálhoz rögzítve a gazdagép eszközeihez és élő statisztikáihoz" + }, + "enableCommandHistory": { + "label": "Parancsnoki előzmények", + "description": "A terminálon futó parancsok rögzítése előzmények és automatikus kiegészítés céljából" + }, + "enableTerminal": { + "label": "Terminál engedélyezése" + } + } + }, + "palette": { + "localTerminal": "Helyi terminál" + } +} diff --git a/plugins/ssh-terminal/locales/translated/id_ID.json b/plugins/ssh-terminal/locales/translated/id_ID.json new file mode 100644 index 000000000..6aaeddd7c --- /dev/null +++ b/plugins/ssh-terminal/locales/translated/id_ID.json @@ -0,0 +1,203 @@ +{ + "mobileKeyboard": { + "addQuickKey": "Menambahkan", + "arrowDown": "Panah ke Bawah", + "arrowLeft": "Panah Kiri", + "arrowRight": "Panah Kanan", + "arrowUp": "Panah Atas", + "backTab": "⇥", + "ctrl": "Ctrl", + "delete": "Del", + "done": "Selesai", + "editQuickKeys": "Edit tombol pintasan", + "end": "Akhir", + "esc": "Esc", + "home": "Rumah", + "pageDown": "PgDn", + "pageUp": "PgUp", + "paste": "Pasta", + "quickKeyPlaceholder": "misalnya sudo ", + "quickKeysDesc": "Ketuk × untuk menghapus. Mendukung hingga 8 karakter.", + "quickKeysTitle": "Tombol Pintas", + "removeQuickKey": "Menghapus", + "resetDefaults": "Kembalikan ke pengaturan default", + "shift": "Menggeser", + "tab": "Tab" + }, + "terminal": { + "autoTmuxEnableFailed": "Gagal mengaktifkan Auto-Tmux", + "autoTmuxEnabled": "Auto-Tmux diaktifkan untuk {{host}}. Ini berlaku mulai koneksi berikutnya.", + "clipboardHttpWarning": "Tidak dapat membaca isi clipboard. Beberapa browser membatasi akses clipboard di luar HTTPS - coba tekan Ctrl+V, atau sajikan Termix melalui HTTPS.", + "closeTab": "Menutup", + "connecting": "Menghubungkan...", + "connectionLost": "Koneksi terputus", + "connectionRejected": "Koneksi ditolak oleh server. Harap periksa otentikasi dan konfigurasi jaringan Anda.", + "connectionTimeout": "Waktu habis koneksi", + "enableAutoTmuxAction": "Aktifkan Auto-Tmux", + "hostKeyRejected": "Verifikasi kunci host SSH ditolak. Koneksi dibatalkan.", + "linkDialogCopy": "Menyalin", + "linkDialogOpen": "Membuka", + "linkDialogTitle": "Buka Tautan", + "maxReconnectAttemptsReached": "Upaya penyambungan kembali maksimum telah tercapai.", + "noHostSelected": "Tidak ada pembawa acara yang dipilih.", + "opksshAuthFailed": "Autentikasi gagal. Harap periksa kredensial Anda dan coba lagi.", + "opksshTimeout": "Autentikasi habis waktu. Silakan coba lagi.", + "passwordPromptFillTitle": "Masukkan kata sandi yang tersimpan ke dalam kolom ini?", + "reconnect": "Terhubung kembali", + "reconnected": "Berhasil terhubung kembali", + "reconnecting": "Menghubungkan kembali... ({{attempt}}/{{max}})", + "searchCaseSensitive": "Kotak Korek Api", + "searchClose": "Tutup (Keluar)", + "searchNext": "Pertandingan Berikutnya (Masuk)", + "searchNoResults": "Tidak ada hasil", + "searchPlaceholder": "Menemukan", + "searchPrevious": "Pertandingan Sebelumnya (Shift+Enter)", + "searchRegex": "Gunakan Ekspresi Reguler", + "searchResultCount": "{{index}} dari {{count}}", + "searchWholeWord": "Cocokkan Seluruh Kata", + "sessionExpiredNotice": "Sesi Anda sebelumnya di {{host}} telah berakhir setelah {{minutes}} menit terputus; shell dan semua yang berjalan di dalamnya telah hilang. Aktifkan Auto-Tmux pada host ini untuk menjaga agar sesi tetap aktif.", + "sessionExpiredNoticeNoMinutes": "Sesi Anda sebelumnya di {{host}} telah berakhir saat terputus; shell dan semua yang berjalan di dalamnya telah hilang. Aktifkan Auto-Tmux pada host ini untuk menjaga agar sesi tetap aktif.", + "sessionTakenOver": "Sesi dibuka di tab lain. Sedang menyambungkan kembali...", + "tmuxAttached": "Klien terlampir", + "tmuxAttachedCount": "{{count}} terlampir", + "tmuxCopyHint": "Sesuaikan pilihan dan tekan Enter untuk menyalin ke papan klip.", + "tmuxCreateNew": "Mulai sesi baru", + "tmuxDetached": "Terputus dari sesi tmux", + "tmuxLastActivity": "Aktivitas terakhir", + "tmuxSessionAttached": "Sesi tmux terlampir: {{name}}", + "tmuxSessionCreated": "Sesi tmux dibuat: {{name}}", + "tmuxSessionPickerDesc": "Sesi tmux yang ada ditemukan di host ini. Pilih salah satu untuk menyambungkan kembali atau membuat sesi baru.", + "tmuxSessionPickerTitle": "Sesi tmux", + "tmuxTimeDays": "{{count}}d lalu", + "tmuxTimeHours": "{{count}}h lalu", + "tmuxTimeJustNow": "baru saja", + "tmuxTimeMinutes": "{{count}}m lalu", + "tmuxUnavailable": "tmux tidak terpasang pada host jarak jauh, sehingga kembali menggunakan shell standar.", + "tmuxWindowCount": "{{count}} jendela", + "tmuxWindows": "Windows", + "unknownError": "Terjadi kesalahan yang tidak diketahui.", + "vaultAuthDescription": "Jendela telah terbuka untuk masuk ke HashiCorp Vault. Selesaikan proses masuk di sana; koneksi ini akan berlanjut secara otomatis.", + "vaultAuthFailed": "Autentikasi Vault gagal. Silakan coba lagi.", + "vaultAuthTitle": "Login ke Vault diperlukan.", + "vaultReopen": "Buka kembali jendela masuk", + "websocketAbnormalClose": "Koneksi terputus secara tidak terduga. Ini mungkin disebabkan oleh masalah konfigurasi reverse proxy atau SSL. Silakan periksa log server.", + "websocketError": "Kesalahan koneksi WebSocket" + }, + "terminalToolbar": { + "detachTmux": "Lepaskan tmux", + "detachTmuxDescription": "Lepaskan sesi tmux", + "dismissError": "Abaikan kesalahan gambar", + "hideToolbar": "Sembunyikan bilah alat", + "image": "Aksi gambar (hanya gambar)", + "imageActionFailed": "Aksi gambar gagal", + "layout": "Mode tampilan bilah alat", + "layoutExpanded": "Diperluas dengan metrik", + "layoutIcon": "Hanya ikon", + "layoutLabeled": "Ikon dan label", + "moveToolbar": "Pindahkan bilah alat", + "moveToolbarHint": "Seret untuk memposisikan ulang bilah alat", + "paste": "Tempel gambar", + "pasteImage": "Tempel gambar dari clipboard (hanya gambar)", + "retry": "Mencoba kembali", + "showToolbar": "Perluas bilah alat", + "upload": "Unggah gambar", + "uploadImage": "Unggah gambar ke host", + "uploadingImage": "Sedang mengunggah..." + }, + "history": { + "noTerminalSelected": "Tidak ada terminal yang dipilih", + "noTerminalSelectedHint": "Buka tab terminal SSH untuk melihat riwayat perintahnya.", + "searchPlaceholder": "Riwayat pencarian...", + "clearAll": "Hapus Semua", + "noHistoryEntries": "Tidak ada entri riwayat.", + "trackingDisabled": "Pelacakan riwayat dinonaktifkan" + }, + "touchInput": { + "enabled": "Aktifkan Input Sentuh", + "enabledDesc": "Izinkan pengguliran sentuh satu jari di terminal.", + "momentumEnabled": "Aktifkan Momentum", + "momentumEnabledDesc": "Lanjutkan menggulir sebentar setelah menggeser", + "advanced": "Canggih", + "resetDefaults": "Kembalikan ke pengaturan default", + "dragThreshold": "Ambang batas seret", + "maxWheelDelta": "Delta roda maksimum", + "momentumSampleWindow": "Jendela sampel momentum", + "releaseGracePeriod": "Masa tenggang pelepasan", + "minimumVelocity": "Kecepatan minimum", + "maximumVelocity": "Kecepatan maksimum", + "maximumDuration": "Durasi maksimum", + "maximumTravel": "Perjalanan maksimum", + "decayTime": "Waktu peluruhan", + "pixelsPerTick": "Piksel per detik", + "maximumFrameInterval": "Interval bingkai maksimum", + "maximumTicksPerFrame": "Jumlah tick maksimum per frame" + }, + "imageStorage": { + "instanceId": "ID instance sesi terminal", + "instanceIdDesc": "ID instans dari salah satu tab terminal yang terhubung, digunakan untuk menguji visibilitas.", + "selectedModeUnavailable": "Tidak tersedia", + "test": "Tes", + "testing": "Pengujian...", + "testFailed": "Pengujian penyimpanan gambar gagal.", + "testConnected": "Sesi terminal terhubung.", + "testNotConnected": "Tidak ada sesi terminal yang terhubung dengan ID instance ini.", + "testSelectedMode": "Unggahan akan menggunakan: {{mode}}", + "testLocalVisible": "Direktori lokal tersebut dapat dilihat dari host yang terhubung.", + "testLocalNotVisible": "Direktori lokal TIDAK terlihat dari host yang terhubung." + }, + "settings": { + "admin": { + "group": { + "touch": "Input Sentuh", + "images": "Penyimpanan Gambar" + }, + "sessionTimeoutMinutes": { + "label": "Ketahanan Sesi Terminal", + "description": "Berapa lama terminal yang terputus tetap aktif di server sebelum ditutup. Minimal 1 menit · Maksimal 1440 menit. Untuk sesi yang harus bertahan lebih lama dari ini, aktifkan Auto-Tmux pada host." + }, + "commandHistoryEnabled": { + "label": "Riwayat Perintah", + "description": "Izinkan perekaman riwayat perintah. Jika dinonaktifkan, riwayat tidak akan disimpan terlepas dari pengaturan per host." + }, + "imageStorageMode": { + "label": "Mode Penyimpanan", + "description": "Di sinilah gambar terminal yang disalin dan diunggah ditulis. Pemilihan otomatis berdasarkan koneksi, Lokal dan Jarak Jauh (SFTP) telah ditetapkan.", + "auto": "Mobil", + "local": "Lokal", + "remoteSftp": "Jarak Jauh (SFTP)" + }, + "imageLocalDir": { + "label": "Direktori lokal" + }, + "imageHostPath": { + "label": "Jalur host", + "description": "Tampilan sisi host dari direktori lokal yang diterima oleh agen terminal." + }, + "imageTtlMs": { + "label": "TTL (ms)" + }, + "imageMaxCount": { + "label": "Jumlah gambar maksimum" + }, + "imageMaxBytes": { + "label": "Byte penyimpanan maksimum" + } + }, + "host": { + "enableTerminalToolbar": { + "label": "Aktifkan Toolbar Terminal", + "description": "Tampilkan bilah alat akses cepat yang terpasang pada terminal aktif untuk alat dan statistik langsung host ini." + }, + "enableCommandHistory": { + "label": "Riwayat Perintah", + "description": "Rekam perintah yang dijalankan di terminal ini untuk riwayat dan pelengkapan otomatis." + }, + "enableTerminal": { + "label": "Aktifkan Terminal" + } + } + }, + "palette": { + "localTerminal": "Terminal Lokal" + } +} diff --git a/plugins/ssh-terminal/locales/translated/it_IT.json b/plugins/ssh-terminal/locales/translated/it_IT.json new file mode 100644 index 000000000..818ca57a5 --- /dev/null +++ b/plugins/ssh-terminal/locales/translated/it_IT.json @@ -0,0 +1,203 @@ +{ + "mobileKeyboard": { + "addQuickKey": "Aggiungi", + "arrowDown": "Freccia Giù", + "arrowLeft": "Freccia Sinistra", + "arrowRight": "Freccia Destra", + "arrowUp": "Freccia Su", + "backTab": "⇥", + "ctrl": "Ctrl", + "delete": "Canc", + "done": "Fine", + "editQuickKeys": "Modifica tasti rapidi", + "end": "Fine", + "esc": "Esc", + "home": "Home", + "pageDown": "Pag Giù", + "pageUp": "Pag Su", + "paste": "Impasto", + "quickKeyPlaceholder": "ad es. sudo ", + "quickKeysDesc": "Tocca × per rimuovere. Supporta fino a 8 caratteri.", + "quickKeysTitle": "Tasti Rapidi", + "removeQuickKey": "Rimuovi", + "resetDefaults": "Ripristina impostazioni predefinite", + "shift": "Maiusc", + "tab": "Tab" + }, + "terminal": { + "autoTmuxEnableFailed": "Impossibile abilitare Auto-Tmux", + "autoTmuxEnabled": "Auto-Tmux abilitato per {{host}}. Si applica dalla prossima connessione.", + "clipboardHttpWarning": "Impossibile leggere gli appunti. Alcuni browser limitano l'accesso agli appunti al di fuori del protocollo HTTPS: prova a usare Ctrl+V oppure a utilizzare Termix tramite HTTPS.", + "closeTab": "Chiudi", + "connecting": "Connessione in corso...", + "connectionLost": "Connessione persa", + "connectionRejected": "Connessione rifiutata dal server. Controlla l'autenticazione e la configurazione di rete.", + "connectionTimeout": "Timeout di connessione", + "enableAutoTmuxAction": "Abilita Auto-Tmux", + "hostKeyRejected": "Verifica della chiave host SSH rifiutata. Connessione annullata.", + "linkDialogCopy": "Copia", + "linkDialogOpen": "Apri", + "linkDialogTitle": "Apri collegamento", + "maxReconnectAttemptsReached": "Raggiunto il numero massimo di tentativi di riconnessione", + "noHostSelected": "Nessun host selezionato", + "opksshAuthFailed": "Autenticazione fallita. Controlla le credenziali e riprova.", + "opksshTimeout": "Autenticazione scaduta. Riprova.", + "passwordPromptFillTitle": "Inserire la password salvata in questa richiesta?", + "reconnect": "Riconnetti", + "reconnected": "Riconnesso con successo", + "reconnecting": "Riconnessione... ({{attempt}}/{{max}})", + "searchCaseSensitive": "Scatola di fiammiferi", + "searchClose": "Chiudi (Esc)", + "searchNext": "Prossima partita (Invio)", + "searchNoResults": "Nessun risultato", + "searchPlaceholder": "Trovare", + "searchPrevious": "Partita precedente (Maiusc+Invio)", + "searchRegex": "Utilizzare le espressioni regolari", + "searchResultCount": "{{index}} di {{count}}", + "searchWholeWord": "Abbina la parola intera", + "sessionExpiredNotice": "La tua sessione precedente su {{host}} è scaduta dopo {{minutes}} minuti di disconnessione; la shell e tutto ciò che era in esecuzione al suo interno sono stati terminati. Abilita Auto-Tmux su questo host per mantenere attive le sessioni.", + "sessionExpiredNoticeNoMinutes": "La tua sessione precedente su {{host}} è scaduta mentre eri disconnesso; la shell e tutto ciò che era in esecuzione al suo interno sono stati eliminati. Abilita Auto-Tmux su questo host per mantenere attive le sessioni.", + "sessionTakenOver": "La sessione è stata aperta in un'altra scheda. Riconnessione...", + "tmuxAttached": "Client collegati", + "tmuxAttachedCount": "{{count}} collegato", + "tmuxCopyHint": "Regola la selezione e premi Invio per copiare negli appunti", + "tmuxCreateNew": "Avvia nuova sessione", + "tmuxDetached": "Scollegato dalla sessione tmux", + "tmuxLastActivity": "Ultima attività", + "tmuxSessionAttached": "Sessione tmux collegata: {{name}}", + "tmuxSessionCreated": "Sessione tmux creata: {{name}}", + "tmuxSessionPickerDesc": "Sono state trovate sessioni tmux esistenti su questo host. Selezionane una per ricollegarti o crea una nuova sessione.", + "tmuxSessionPickerTitle": "Sessioni tmux", + "tmuxTimeDays": "{{count}}d fa", + "tmuxTimeHours": "{{count}}h fa", + "tmuxTimeJustNow": "Adesso", + "tmuxTimeMinutes": "{{count}}m fa", + "tmuxUnavailable": "tmux non è installato sull'host remoto, si utilizza la shell standard", + "tmuxWindowCount": "{{count}} finestra", + "tmuxWindows": "Finestre", + "unknownError": "Si è verificato un errore sconosciuto", + "vaultAuthDescription": "Una finestra si è aperta per accedere a HashiCorp Vault. Completa l'accesso lì; la connessione continuerà automaticamente.", + "vaultAuthFailed": "Autenticazione Vault fallita. Riprova.", + "vaultAuthTitle": "Accesso a Vault richiesto", + "vaultReopen": "Riapri la finestra di accesso", + "websocketAbnormalClose": "La connessione si è chiusa in modo imprevisto. Potrebbe trattarsi di un problema di reverse proxy o configurazione SSL. Controlla i log del server.", + "websocketError": "Errore di connessione WebSocket" + }, + "terminalToolbar": { + "detachTmux": "Scollega tmux", + "detachTmuxDescription": "Scollega la sessione tmux", + "dismissError": "Chiudi errore immagine", + "hideToolbar": "Nascondi la barra degli strumenti", + "image": "Azioni sulle immagini (solo immagini)", + "imageActionFailed": "L'azione sull'immagine non è riuscita.", + "layout": "Modalità di visualizzazione della barra degli strumenti", + "layoutExpanded": "Espandeto con metriche", + "layoutIcon": "Solo icona", + "layoutLabeled": "Icone ed etichette", + "moveToolbar": "Sposta la barra degli strumenti", + "moveToolbarHint": "Trascina per riposizionare la barra degli strumenti", + "paste": "Incolla l'immagine", + "pasteImage": "Incolla l'immagine dagli appunti (solo immagini)", + "retry": "Riprova", + "showToolbar": "Espandi la barra degli strumenti", + "upload": "Carica immagine", + "uploadImage": "Carica un'immagine sull'host", + "uploadingImage": "Caricamento in corso..." + }, + "history": { + "noTerminalSelected": "Nessun terminale selezionato", + "noTerminalSelectedHint": "Apri una scheda di terminale SSH per visualizzare la cronologia dei comandi", + "searchPlaceholder": "Cerca nella cronologia...", + "clearAll": "Cancella tutto", + "noHistoryEntries": "Nessuna voce nella cronologia", + "trackingDisabled": "Tracciamento cronologia disabilitato" + }, + "touchInput": { + "enabled": "Abilita input tattile", + "enabledDesc": "Consenti lo scorrimento con un solo dito nei terminali", + "momentumEnabled": "Abilitare lo slancio", + "momentumEnabledDesc": "Continua a scorrere brevemente dopo uno swipe", + "advanced": "Avanzato", + "resetDefaults": "Ripristina le impostazioni predefinite", + "dragThreshold": "Trascinamento", + "maxWheelDelta": "Delta massimo della ruota", + "momentumSampleWindow": "Finestra di campionamento del momento", + "releaseGracePeriod": "Periodo di grazia per il rilascio", + "minimumVelocity": "Velocità minima", + "maximumVelocity": "velocità massima", + "maximumDuration": "Durata massima", + "maximumTravel": "Viaggio massimo", + "decayTime": "tempo di decadimento", + "pixelsPerTick": "Pixel per tick", + "maximumFrameInterval": "Intervallo massimo tra i fotogrammi", + "maximumTicksPerFrame": "Numero massimo di tick per fotogramma" + }, + "imageStorage": { + "instanceId": "ID sessione terminale", + "instanceIdDesc": "ID istanza di una delle schede del terminale a cui sei connesso, utilizzato per testare la visibilità.", + "selectedModeUnavailable": "Non disponibile", + "test": "Test", + "testing": "Test...", + "testFailed": "Impossibile testare l'archiviazione delle immagini", + "testConnected": "La sessione del terminale è connessa.", + "testNotConnected": "Nessuna sessione terminale connessa con questo ID istanza.", + "testSelectedMode": "Per caricare un file si utilizzerebbe: {{mode}}", + "testLocalVisible": "La directory locale è visibile dall'host connesso.", + "testLocalNotVisible": "La directory locale NON è visibile dall'host connesso." + }, + "settings": { + "admin": { + "group": { + "touch": "Input tattile", + "images": "Archiviazione delle immagini" + }, + "sessionTimeoutMinutes": { + "label": "Persistenza della sessione terminale", + "description": "Per quanto tempo un terminale disconnesso rimane attivo sul server prima di essere chiuso. Minimo 1 minuto · Massimo 1440 minuti. Per le sessioni che devono durare più a lungo, abilitare Auto-Tmux sull'host." + }, + "commandHistoryEnabled": { + "label": "Cronologia comandi", + "description": "Consenti la registrazione della cronologia comandi. Se disabilitato, la cronologia non viene salvata indipendentemente dalle impostazioni per host." + }, + "imageStorageMode": { + "label": "Modalità di archiviazione", + "description": "Dove vengono scritte le immagini del terminale incollate e caricate. Le selezioni automatiche per connessione, locale e remota (SFTP), sono fisse.", + "auto": "Auto", + "local": "Locale", + "remoteSftp": "Remoto (SFTP)" + }, + "imageLocalDir": { + "label": "Cartella locale" + }, + "imageHostPath": { + "label": "Percorso host", + "description": "La visualizzazione lato host della directory locale ricevuta dagli agenti terminali." + }, + "imageTtlMs": { + "label": "TTL (ms)" + }, + "imageMaxCount": { + "label": "Numero massimo di immagini" + }, + "imageMaxBytes": { + "label": "byte di archiviazione massimi" + } + }, + "host": { + "enableTerminalToolbar": { + "label": "Abilita la barra degli strumenti del terminale", + "description": "Mostra una barra degli strumenti ad accesso rapido ancorata al terminale attivo per gli strumenti e le statistiche in tempo reale di questo host." + }, + "enableCommandHistory": { + "label": "Cronologia comandi", + "description": "Registra i comandi eseguiti in questo terminale per la cronologia e il completamento automatico" + }, + "enableTerminal": { + "label": "Abilita Terminale" + } + } + }, + "palette": { + "localTerminal": "Terminale locale" + } +} diff --git a/plugins/ssh-terminal/locales/translated/ja_JP.json b/plugins/ssh-terminal/locales/translated/ja_JP.json new file mode 100644 index 000000000..60f1b8568 --- /dev/null +++ b/plugins/ssh-terminal/locales/translated/ja_JP.json @@ -0,0 +1,203 @@ +{ + "mobileKeyboard": { + "addQuickKey": "追加", + "arrowDown": "↓", + "arrowLeft": "←", + "arrowRight": "→", + "arrowUp": "↑", + "backTab": "⇥", + "ctrl": "Ctrl", + "delete": "Del", + "done": "完了", + "editQuickKeys": "クイックキー編集", + "end": "エンド", + "esc": "Esc", + "home": "ホーム", + "pageDown": "PgDn", + "pageUp": "PgUp", + "paste": "ペースト", + "quickKeyPlaceholder": "例: sudo ", + "quickKeysDesc": "× をタップして削除します。最大8文字まで対応。", + "quickKeysTitle": "クイックキー", + "removeQuickKey": "削除", + "resetDefaults": "デフォルトにリセット", + "shift": "Shift", + "tab": "Tab" + }, + "terminal": { + "autoTmuxEnableFailed": "Auto-Tmux の有効化に失敗しました", + "autoTmuxEnabled": "{{host}}に対して Auto-Tmux が有効になりました。これは次回の接続時に適用されます。", + "clipboardHttpWarning": "クリップボードを読み取れませんでした。一部のブラウザはHTTPS接続以外でのクリップボードへのアクセスを制限しています。代わりにCtrl+Vを試すか、TermixをHTTPS経由で提供してみてください。", + "closeTab": "閉じる", + "connecting": "接続中...", + "connectionLost": "接続切断", + "connectionRejected": "サーバーによって接続が拒否されました。認証設定とネットワーク構成を確認してください。", + "connectionTimeout": "接続タイムアウト", + "enableAutoTmuxAction": "Auto-Tmuxを有効にする", + "hostKeyRejected": "SSHホストキーの検証が拒否されました。接続がキャンセルされました。", + "linkDialogCopy": "コピー", + "linkDialogOpen": "開く", + "linkDialogTitle": "リンクを開く", + "maxReconnectAttemptsReached": "再接続の最大試行回数に達しました", + "noHostSelected": "ホストが選択されていません", + "opksshAuthFailed": "認証に失敗しました。資格情報を確認して再試行してください。", + "opksshTimeout": "認証がタイムアウトしました。もう一度お試しください。", + "passwordPromptFillTitle": "保存されたパスワードをこのプロンプトに入力しますか?", + "reconnect": "再接続", + "reconnected": "再接続完了", + "reconnecting": "再接続中... ({{attempt}}/{{max}})", + "searchCaseSensitive": "マッチケース", + "searchClose": "閉じる(Escキー)", + "searchNext": "次の試合(入場)", + "searchNoResults": "検索結果なし", + "searchPlaceholder": "探す", + "searchPrevious": "前の試合 (Shift+Enter)", + "searchRegex": "正規表現を使用する", + "searchResultCount": "{{index}} / {{count}}", + "searchWholeWord": "単語全体を一致させる", + "sessionExpiredNotice": "{{host}} での前回のセッションは {{minutes}} 分後に切断され、シェルおよびそこで実行されていたすべてのものが失われました。セッションを維持するには、このホストで Auto-Tmux を有効にしてください。", + "sessionExpiredNoticeNoMinutes": "{{host}} での前回のセッションは、接続が切断された状態で期限切れになりました。シェルおよびシェル内で実行されていたすべてのプロセスは失われています。セッションを維持するには、このホストで Auto-Tmux を有効にしてください。", + "sessionTakenOver": "セッションが別のタブで開かれました。再接続中...", + "tmuxAttached": "接続クライアント", + "tmuxAttachedCount": "{{count}} 接続", + "tmuxCopyHint": "選択範囲を調整してEnterでクリップボードにコピー", + "tmuxCreateNew": "新規セッションを開始", + "tmuxDetached": "tmuxセッションからデタッチしました", + "tmuxLastActivity": "最終アクティビティ", + "tmuxSessionAttached": "tmuxセッションに接続しました: {{name}}", + "tmuxSessionCreated": "tmuxセッションが作成されました: {{name}}", + "tmuxSessionPickerDesc": "このホストに既存のtmuxセッションがあります。再アタッチするか、新しいセッションを作成してください。", + "tmuxSessionPickerTitle": "tmuxセッション", + "tmuxTimeDays": "{{count}}日前", + "tmuxTimeHours": "{{count}}時間前", + "tmuxTimeJustNow": "たった今", + "tmuxTimeMinutes": "{{count}}分前", + "tmuxUnavailable": "リモートホストにtmuxがインストールされていません。標準シェルにフォールバックします。", + "tmuxWindowCount": "{{count}} ウィンドウ", + "tmuxWindows": "ウィンドウ", + "unknownError": "不明なエラーが発生しました", + "vaultAuthDescription": "HashiCorp Vaultへのサインイン用のウィンドウが開きました。そこでサインインを完了すると、この接続は自動的に続行されます。", + "vaultAuthFailed": "Vault 認証に失敗しました。もう一度お試しください。", + "vaultAuthTitle": "Vault サインインが必要です", + "vaultReopen": "サインインウィンドウを再表示", + "websocketAbnormalClose": "接続が予期せず切断されました。リバースプロキシまたはSSLの設定に問題がある可能性があります。サーバーログを確認してください。", + "websocketError": "WebSocket接続エラー" + }, + "terminalToolbar": { + "detachTmux": "tmuxをデタッチする", + "detachTmuxDescription": "tmuxセッションをデタッチする", + "dismissError": "画像エラーを非表示にする", + "hideToolbar": "ツールバーを非表示にする", + "image": "画像アクション(画像のみ)", + "imageActionFailed": "画像操作が失敗しました", + "layout": "ツールバー表示モード", + "layoutExpanded": "指標を追加して拡張", + "layoutIcon": "アイコンのみ", + "layoutLabeled": "アイコンとラベル", + "moveToolbar": "ツールバーを移動", + "moveToolbarHint": "ツールバーをドラッグして位置を変更します", + "paste": "画像を貼り付ける", + "pasteImage": "クリップボードから画像を貼り付ける(画像のみ)", + "retry": "リトライ", + "showToolbar": "ツールバーを展開する", + "upload": "画像をアップロード", + "uploadImage": "ホストに画像をアップロードする", + "uploadingImage": "アップロード中..." + }, + "history": { + "noTerminalSelected": "ターミナルが選択されていません", + "noTerminalSelectedHint": "SSH ターミナルタブを開いてコマンド履歴を表示", + "searchPlaceholder": "履歴を検索...", + "clearAll": "すべてクリア", + "noHistoryEntries": "履歴エントリがありません", + "trackingDisabled": "履歴の追跡が無効です" + }, + "touchInput": { + "enabled": "タッチ入力を有効にする", + "enabledDesc": "端末で指一本でのスクロールを可能にする", + "momentumEnabled": "勢いを加速させる", + "momentumEnabledDesc": "スワイプ後、少しスクロールを続ける", + "advanced": "高度な", + "resetDefaults": "初期設定に戻す", + "dragThreshold": "ドラッグしきい値", + "maxWheelDelta": "最大ホイールデルタ", + "momentumSampleWindow": "モメンタムサンプルウィンドウ", + "releaseGracePeriod": "釈放猶予期間", + "minimumVelocity": "最小速度", + "maximumVelocity": "最大速度", + "maximumDuration": "最大持続時間", + "maximumTravel": "最大移動距離", + "decayTime": "減衰時間", + "pixelsPerTick": "ピクセル/ティック", + "maximumFrameInterval": "最大フレーム間隔", + "maximumTicksPerFrame": "フレームあたりの最大ティック数" + }, + "imageStorage": { + "instanceId": "端末セッションインスタンスID", + "instanceIdDesc": "表示されている可視性をテストするために使用される、接続されているターミナルタブのいずれかのインスタンスID。", + "selectedModeUnavailable": "利用不可", + "test": "テスト", + "testing": "テスト中...", + "testFailed": "画像ストレージのテストに失敗しました", + "testConnected": "端末セッションが接続されました。", + "testNotConnected": "このインスタンスIDで接続されている端末セッションはありません。", + "testSelectedMode": "アップロードには以下を使用します: {{mode}}", + "testLocalVisible": "ローカルディレクトリは、接続されているホストから参照可能です。", + "testLocalNotVisible": "ローカルディレクトリは、接続されているホストからアクセスできません。" + }, + "settings": { + "admin": { + "group": { + "touch": "タッチ入力", + "images": "画像ストレージ" + }, + "sessionTimeoutMinutes": { + "label": "端末セッションの永続性", + "description": "切断された端末がサーバー上で閉じられるまでの時間。最小1分、最大1440分。この時間を超えてセッションを継続する必要がある場合は、ホスト上でAuto-Tmuxを有効にしてください。" + }, + "commandHistoryEnabled": { + "label": "コマンド履歴", + "description": "コマンド履歴の記録を許可します。無効にすると、ホストごとの設定に関わらず履歴は保存されません。" + }, + "imageStorageMode": { + "label": "ストレージモード", + "description": "貼り付けおよびアップロードされた端末イメージが書き込まれる場所。接続による自動選択、ローカルおよびリモート(SFTP)が固定されています。", + "auto": "自動", + "local": "地元", + "remoteSftp": "リモート(SFTP)" + }, + "imageLocalDir": { + "label": "ローカルディレクトリ" + }, + "imageHostPath": { + "label": "ホストパス", + "description": "端末エージェントが受信するローカルディレクトリのホスト側ビュー。" + }, + "imageTtlMs": { + "label": "TTL(ミリ秒)" + }, + "imageMaxCount": { + "label": "最大画像数" + }, + "imageMaxBytes": { + "label": "最大ストレージバイト数" + } + }, + "host": { + "enableTerminalToolbar": { + "label": "ターミナルツールバーを有効にする", + "description": "このホストのツールとリアルタイム統計情報に素早くアクセスできるツールバーを、アクティブなターミナルにドッキングして表示します。" + }, + "enableCommandHistory": { + "label": "コマンド履歴", + "description": "このターミナルで実行されたコマンドを履歴とオートコンプリート用に記録します" + }, + "enableTerminal": { + "label": "ターミナルを有効にする" + } + } + }, + "palette": { + "localTerminal": "ローカル端末" + } +} diff --git a/plugins/ssh-terminal/locales/translated/ko_KR.json b/plugins/ssh-terminal/locales/translated/ko_KR.json new file mode 100644 index 000000000..9bb4dd713 --- /dev/null +++ b/plugins/ssh-terminal/locales/translated/ko_KR.json @@ -0,0 +1,203 @@ +{ + "mobileKeyboard": { + "addQuickKey": "추가", + "arrowDown": "아래쪽 화살표", + "arrowLeft": "왼쪽 화살표", + "arrowRight": "오른쪽 화살표", + "arrowUp": "위쪽 화살표", + "backTab": "⇥", + "ctrl": "Ctrl", + "delete": "Del", + "done": "완료", + "editQuickKeys": "빠른 키 편집", + "end": "End", + "esc": "Esc", + "home": "Home", + "pageDown": "PgDn", + "pageUp": "PgUp", + "paste": "반죽", + "quickKeyPlaceholder": "예: sudo ", + "quickKeysDesc": "×를 탭하여 제거합니다. 최대 8자까지 지원합니다.", + "quickKeysTitle": "빠른 키", + "removeQuickKey": "제거", + "resetDefaults": "기본값으로 초기화", + "shift": "Shift", + "tab": "Tab" + }, + "terminal": { + "autoTmuxEnableFailed": "자동 Tmux를 활성화하는 데 실패했습니다.", + "autoTmuxEnabled": "{{host}}에 대해 자동 Tmux가 활성화되었습니다. 이는 다음 연결부터 적용됩니다.", + "clipboardHttpWarning": "클립보드를 읽을 수 없습니다. 일부 브라우저는 HTTPS 외부에서 클립보드 접근을 제한합니다. Ctrl+V를 눌러 V를 누르거나 Termix를 HTTPS로 실행해 보세요.", + "closeTab": "닫기", + "connecting": "연결 중...", + "connectionLost": "연결이 끊어졌습니다", + "connectionRejected": "서버에서 연결이 거부되었습니다. 인증 및 네트워크 구성을 확인하세요.", + "connectionTimeout": "연결 시간 초과", + "enableAutoTmuxAction": "자동 Tmux 활성화", + "hostKeyRejected": "SSH 호스트 키 검증이 거부되었습니다. 연결이 취소되었습니다.", + "linkDialogCopy": "복사", + "linkDialogOpen": "열기", + "linkDialogTitle": "링크 열기", + "maxReconnectAttemptsReached": "최대 재연결 시도 횟수에 도달했습니다.", + "noHostSelected": "선택된 호스트가 없습니다.", + "opksshAuthFailed": "인증에 실패했습니다. 자격 증명을 확인하고 다시 시도하세요.", + "opksshTimeout": "인증 시간이 초과되었습니다. 다시 시도하세요.", + "passwordPromptFillTitle": "저장된 비밀번호를 이 프롬프트에 입력하시겠습니까?", + "reconnect": "재연결", + "reconnected": "재연결 성공", + "reconnecting": "재연결 중... ({{attempt}}/{{max}})", + "searchCaseSensitive": "대소문자를 일치시키세요", + "searchClose": "닫기 (탈출)", + "searchNext": "다음 매치 (Enter)", + "searchNoResults": "결과가 없습니다", + "searchPlaceholder": "찾다", + "searchPrevious": "이전 일치 항목 (Shift+Enter)", + "searchRegex": "정규 표현식을 사용하세요", + "searchResultCount": "{{index}} {{count}}", + "searchWholeWord": "단어 전체를 일치시키세요", + "sessionExpiredNotice": "{{host}} 호스트에서 진행 중이던 이전 세션이 {{minutes}} 분 후 연결이 끊어지면서 만료되었습니다. 셸과 셸에서 실행 중이던 모든 항목이 종료되었습니다. 세션을 유지하려면 이 호스트에서 자동 Tmux를 활성화하십시오.", + "sessionExpiredNoticeNoMinutes": "{{host}} 에서 이전 세션이 연결이 끊긴 상태에서 만료되었습니다. 셸과 그 안에서 실행 중이던 모든 항목이 사라졌습니다. 세션을 유지하려면 이 호스트에서 자동 Tmux를 활성화하십시오.", + "sessionTakenOver": "다른 탭에서 세션이 열렸습니다. 다시 연결 중...", + "tmuxAttached": "연결된 클라이언트", + "tmuxAttachedCount": "{{count}}개 연결됨", + "tmuxCopyHint": "선택 영역을 조정하고 Enter 키를 누르면 클립보드에 복사됩니다.", + "tmuxCreateNew": "새 세션 시작", + "tmuxDetached": "tmux 세션에서 분리됨", + "tmuxLastActivity": "마지막 활동", + "tmuxSessionAttached": "tmux 세션 연결됨: {{name}}", + "tmuxSessionCreated": "tmux 세션 생성됨: {{name}}", + "tmuxSessionPickerDesc": "이 호스트에서 기존 tmux 세션을 찾았습니다. 다시 연결할 세션을 선택하거나 새 세션을 만드세요.", + "tmuxSessionPickerTitle": "tmux 세션", + "tmuxTimeDays": "{{count}}일 전", + "tmuxTimeHours": "{{count}}시간 전", + "tmuxTimeJustNow": "방금", + "tmuxTimeMinutes": "{{count}}분 전", + "tmuxUnavailable": "원격 호스트에 tmux가 설치되어 있지 않아 표준 셸로 대체합니다.", + "tmuxWindowCount": "{{count}}개 윈도우", + "tmuxWindows": "윈도우", + "unknownError": "알 수 없는 오류가 발생했습니다.", + "vaultAuthDescription": "HashiCorp Vault 로그인을 위한 창이 열렸습니다. 해당 창에서 로그인을 완료하면 연결이 자동으로 계속됩니다.", + "vaultAuthFailed": "Vault 인증에 실패했습니다. 다시 시도하세요.", + "vaultAuthTitle": "Vault 로그인 필요", + "vaultReopen": "로그인 창 다시 열기", + "websocketAbnormalClose": "연결이 예기치 않게 종료되었습니다. 리버스 프록시 또는 SSL 구성 문제일 수 있습니다. 서버 로그를 확인하세요.", + "websocketError": "WebSocket 연결 오류" + }, + "terminalToolbar": { + "detachTmux": "tmux 분리", + "detachTmuxDescription": "tmux 세션 분리", + "dismissError": "이미지 오류를 닫습니다", + "hideToolbar": "툴바 숨기기", + "image": "이미지 관련 작업 (이미지에만 해당)", + "imageActionFailed": "이미지 작업이 실패했습니다.", + "layout": "툴바 표시 모드", + "layoutExpanded": "측정 지표를 추가하여 확장했습니다.", + "layoutIcon": "아이콘만", + "layoutLabeled": "아이콘 및 레이블", + "moveToolbar": "툴바 이동", + "moveToolbarHint": "드래그하여 툴바의 위치를 변경하세요.", + "paste": "이미지 붙여넣기", + "pasteImage": "클립보드에서 이미지 붙여넣기 (이미지만 해당)", + "retry": "다시 해 보다", + "showToolbar": "확장 도구 모음", + "upload": "이미지 업로드", + "uploadImage": "호스트에 이미지를 업로드하세요", + "uploadingImage": "업로드 중..." + }, + "history": { + "noTerminalSelected": "선택된 터미널 없음", + "noTerminalSelectedHint": "SSH 터미널 탭을 열어 명령 기록을 확인하세요.", + "searchPlaceholder": "기록 검색...", + "clearAll": "모두 지우기", + "noHistoryEntries": "기록 항목 없음", + "trackingDisabled": "기록 추적이 비활성화됨" + }, + "touchInput": { + "enabled": "터치 입력 활성화", + "enabledDesc": "단말기에서 한 손가락 터치 스크롤을 허용합니다.", + "momentumEnabled": "모멘텀을 활성화하세요", + "momentumEnabledDesc": "스와이프 후 잠시 스크롤을 더 내리세요.", + "advanced": "고급의", + "resetDefaults": "기본 설정으로 재설정", + "dragThreshold": "드래그 임계값", + "maxWheelDelta": "최대 휠 델타", + "momentumSampleWindow": "모멘텀 샘플 윈도우", + "releaseGracePeriod": "릴리스 유예 기간", + "minimumVelocity": "최소 속도", + "maximumVelocity": "최대 속도", + "maximumDuration": "최대 지속 시간", + "maximumTravel": "최대 이동", + "decayTime": "소멸 시간", + "pixelsPerTick": "틱당 픽셀", + "maximumFrameInterval": "최대 프레임 간격", + "maximumTicksPerFrame": "프레임당 최대 틱 수" + }, + "imageStorage": { + "instanceId": "터미널 세션 인스턴스 ID", + "instanceIdDesc": "연결된 터미널 탭 중 하나의 인스턴스 ID입니다. 이는 가시성 테스트에 사용됩니다.", + "selectedModeUnavailable": "없는", + "test": "시험", + "testing": "테스트 중...", + "testFailed": "이미지 저장소 테스트에 실패했습니다.", + "testConnected": "터미널 세션이 연결되었습니다.", + "testNotConnected": "이 인스턴스 ID로 연결된 터미널 세션이 없습니다.", + "testSelectedMode": "업로드 시에는 {{mode}}를 사용합니다.", + "testLocalVisible": "로컬 디렉터리는 연결된 호스트에서 볼 수 있습니다.", + "testLocalNotVisible": "로컬 디렉터리는 연결된 호스트에서 볼 수 없습니다." + }, + "settings": { + "admin": { + "group": { + "touch": "터치 입력", + "images": "이미지 저장" + }, + "sessionTimeoutMinutes": { + "label": "터미널 세션 지속성", + "description": "연결이 끊긴 터미널이 서버에서 종료되기 전까지 유지되는 시간입니다. 최소 1분, 최대 1440분. 이 시간보다 오래 세션을 유지해야 하는 경우 호스트에서 자동 Tmux를 활성화하십시오." + }, + "commandHistoryEnabled": { + "label": "명령어 히스토리", + "description": "명령어 히스토리 기록 허용. 비활성화 시 호스트별 설정과 무관하게 히스토리가 저장되지 않습니다." + }, + "imageStorageMode": { + "label": "저장 모드", + "description": "붙여넣거나 업로드한 터미널 이미지가 표시되는 위치입니다. 연결 유형에 따라 로컬 및 원격(SFTP) 이미지가 자동으로 선택됩니다.", + "auto": "자동", + "local": "현지의", + "remoteSftp": "원격(SFTP)" + }, + "imageLocalDir": { + "label": "로컬 디렉토리" + }, + "imageHostPath": { + "label": "호스트 경로", + "description": "터미널 에이전트가 수신하는 로컬 디렉터리의 호스트 측 보기입니다." + }, + "imageTtlMs": { + "label": "TTL(밀리초)" + }, + "imageMaxCount": { + "label": "최대 이미지 개수" + }, + "imageMaxBytes": { + "label": "최대 저장 용량(바이트)" + } + }, + "host": { + "enableTerminalToolbar": { + "label": "터미널 툴바 활성화", + "description": "현재 활성화된 터미널에 도킹된 빠른 액세스 툴바를 표시하여 해당 호스트의 도구 및 실시간 통계를 볼 수 있도록 합니다." + }, + "enableCommandHistory": { + "label": "명령어 히스토리", + "description": "히스토리 및 자동 완성을 위해 이 터미널에서 실행된 명령어를 기록합니다" + }, + "enableTerminal": { + "label": "터미널 활성화" + } + } + }, + "palette": { + "localTerminal": "로컬 터미널" + } +} diff --git a/plugins/ssh-terminal/locales/translated/nl_NL.json b/plugins/ssh-terminal/locales/translated/nl_NL.json new file mode 100644 index 000000000..ec20f1d56 --- /dev/null +++ b/plugins/ssh-terminal/locales/translated/nl_NL.json @@ -0,0 +1,203 @@ +{ + "mobileKeyboard": { + "addQuickKey": "Toevoegen", + "arrowDown": "Pijl omlaag", + "arrowLeft": "Pijl naar links", + "arrowRight": "Pijl naar rechts", + "arrowUp": "Pijl omhoog", + "backTab": "⇥", + "ctrl": "Ctrl", + "delete": "Del", + "done": "Klaar", + "editQuickKeys": "Sneltoetsen bewerken", + "end": "Einde", + "esc": "Esc", + "home": "Thuis", + "pageDown": "PgDn", + "pageUp": "PgUp", + "paste": "Plakken", + "quickKeyPlaceholder": "bijv. sudo ", + "quickKeysDesc": "Tik op × om te verwijderen. Ondersteunt maximaal 8 tekens.", + "quickKeysTitle": "Sneltoetsen", + "removeQuickKey": "Verwijderen", + "resetDefaults": "Terugzetten naar standaardinstellingen", + "shift": "Verschuiving", + "tab": "Tab" + }, + "terminal": { + "autoTmuxEnableFailed": "Auto-Tmux inschakelen is mislukt.", + "autoTmuxEnabled": "Auto-Tmux ingeschakeld voor {{host}}. Dit geldt vanaf de volgende verbinding.", + "clipboardHttpWarning": "Kon het klembord niet lezen. Sommige browsers beperken de toegang tot het klembord buiten HTTPS - probeer in plaats daarvan Ctrl+V, of serveer Termix via HTTPS.", + "closeTab": "Dichtbij", + "connecting": "Verbinden...", + "connectionLost": "Verbinding verbroken", + "connectionRejected": "Verbinding geweigerd door de server. Controleer uw authenticatie- en netwerkconfiguratie.", + "connectionTimeout": "Verbindingstime-out", + "enableAutoTmuxAction": "Auto-Tmux inschakelen", + "hostKeyRejected": "SSH-hostsleutelverificatie geweigerd. Verbinding verbroken.", + "linkDialogCopy": "Kopiëren", + "linkDialogOpen": "Open", + "linkDialogTitle": "Open link", + "maxReconnectAttemptsReached": "Maximaal aantal herverbindingspogingen bereikt", + "noHostSelected": "Geen host geselecteerd", + "opksshAuthFailed": "Authenticatie mislukt. Controleer uw inloggegevens en probeer het opnieuw.", + "opksshTimeout": "De authenticatie is mislukt vanwege een time-out. Probeer het opnieuw.", + "passwordPromptFillTitle": "Voer het opgeslagen wachtwoord in op dit veld.", + "reconnect": "Opnieuw verbinding maken", + "reconnected": "De verbinding is succesvol hersteld.", + "reconnecting": "Opnieuw verbinding maken... ({{attempt}}/{{max}})", + "searchCaseSensitive": "Lucifersdoosje", + "searchClose": "Sluiten (Escape)", + "searchNext": "Volgende wedstrijd (Invoeren)", + "searchNoResults": "Geen resultaten", + "searchPlaceholder": "Vinden", + "searchPrevious": "Vorige wedstrijd (Shift+Enter)", + "searchRegex": "Gebruik een reguliere expressie.", + "searchResultCount": "{{index}} van {{count}}", + "searchWholeWord": "Combineer het hele woord", + "sessionExpiredNotice": "Je vorige sessie op {{host}} is na {{minutes}} minuten verlopen; de shell en alles wat erin draaide, is verdwenen. Schakel Auto-Tmux op deze host in om sessies actief te houden.", + "sessionExpiredNoticeNoMinutes": "Je vorige sessie op {{host}} is verlopen terwijl de verbinding verbroken was; de shell en alles wat daarin draaide, is verdwenen. Schakel Auto-Tmux in op deze host om sessies actief te houden.", + "sessionTakenOver": "De sessie is in een ander tabblad geopend. Opnieuw verbinding maken...", + "tmuxAttached": "Aangesloten klanten", + "tmuxAttachedCount": "{{count}} bevestigd", + "tmuxCopyHint": "Pas je selectie aan en druk op Enter om naar het klembord te kopiëren.", + "tmuxCreateNew": "Start een nieuwe sessie", + "tmuxDetached": "Losgekoppeld van de tmux-sessie", + "tmuxLastActivity": "Laatste activiteit", + "tmuxSessionAttached": "tmux-sessie gekoppeld: {{name}}", + "tmuxSessionCreated": "tmux-sessie aangemaakt: {{name}}", + "tmuxSessionPickerDesc": "Bestaande tmux-sessies gevonden op deze host. Selecteer er een om opnieuw verbinding te maken of een nieuwe sessie te creëren.", + "tmuxSessionPickerTitle": "tmux-sessies", + "tmuxTimeDays": "{{count}}dagen geleden", + "tmuxTimeHours": "{{count}}uur geleden", + "tmuxTimeJustNow": "zojuist", + "tmuxTimeMinutes": "{{count}}m geleden", + "tmuxUnavailable": "Tmux is niet geïnstalleerd op de externe host, er wordt teruggevallen op de standaard shell.", + "tmuxWindowCount": "{{count}} venster", + "tmuxWindows": "Windows", + "unknownError": "Er is een onbekende fout opgetreden.", + "vaultAuthDescription": "Er is een venster geopend om in te loggen bij HashiCorp Vault. Voltooi de aanmelding daar; deze verbinding wordt automatisch tot stand gebracht.", + "vaultAuthFailed": "Authenticatie voor de kluis is mislukt. Probeer het opnieuw.", + "vaultAuthTitle": "Aanmelden bij Vault is vereist.", + "vaultReopen": "Heropen het aanmeldvenster", + "websocketAbnormalClose": "De verbinding is onverwacht verbroken. Dit kan te wijten zijn aan een probleem met de reverse proxy of de SSL-configuratie. Controleer de serverlogboeken.", + "websocketError": "WebSocket-verbindingsfout" + }, + "terminalToolbar": { + "detachTmux": "Ontkoppel tmux", + "detachTmuxDescription": "Ontkoppel de tmux-sessie", + "dismissError": "Afbeeldingsfout negeren", + "hideToolbar": "Werkbalk verbergen", + "image": "Acties met afbeeldingen (alleen afbeeldingen)", + "imageActionFailed": "Afbeeldingsactie mislukt", + "layout": "Werkbalkweergavemodus", + "layoutExpanded": "Uitgebreid met meetgegevens", + "layoutIcon": "Alleen pictogram", + "layoutLabeled": "Pictogrammen en labels", + "moveToolbar": "Verplaats de werkbalk", + "moveToolbarHint": "Sleep om de werkbalk te verplaatsen.", + "paste": "Plak afbeelding", + "pasteImage": "Plak een afbeelding vanuit het klembord (alleen afbeeldingen)", + "retry": "Opnieuw proberen", + "showToolbar": "Werkbalk uitklappen", + "upload": "Afbeelding uploaden", + "uploadImage": "Upload een afbeelding naar de host.", + "uploadingImage": "Bezig met uploaden..." + }, + "history": { + "noTerminalSelected": "Geen terminal geselecteerd", + "noTerminalSelectedHint": "Open een SSH-terminaltabblad om de opdrachtgeschiedenis te bekijken.", + "searchPlaceholder": "Zoekgeschiedenis...", + "clearAll": "Alles wissen", + "noHistoryEntries": "Geen geschiedenisvermeldingen", + "trackingDisabled": "Geschiedenisregistratie is uitgeschakeld" + }, + "touchInput": { + "enabled": "Aanraakinvoer inschakelen", + "enabledDesc": "Schakel scrollen met één vinger in op terminals in.", + "momentumEnabled": "Activeer momentum", + "momentumEnabledDesc": "Scroll nog even kort verder na een veegbeweging.", + "advanced": "Geavanceerd", + "resetDefaults": "Terugzetten naar standaardinstellingen", + "dragThreshold": "Sleepdrempel", + "maxWheelDelta": "Maximale wieldelta", + "momentumSampleWindow": "Momentum-steekproefvenster", + "releaseGracePeriod": "Vrijlatingstermijn", + "minimumVelocity": "Minimale snelheid", + "maximumVelocity": "Maximale snelheid", + "maximumDuration": "Maximale duur", + "maximumTravel": "Maximale reis", + "decayTime": "Vervaltijd", + "pixelsPerTick": "Pixels per tik", + "maximumFrameInterval": "Maximale frame-interval", + "maximumTicksPerFrame": "Maximaal aantal ticks per frame" + }, + "imageStorage": { + "instanceId": "Terminal sessie instantie-ID", + "instanceIdDesc": "Instantie-ID van een van uw verbonden terminaltabbladen, gebruikt om de zichtbaarheid te testen.", + "selectedModeUnavailable": "Niet beschikbaar", + "test": "Test", + "testing": "Testen...", + "testFailed": "Het testen van de beeldopslag is mislukt.", + "testConnected": "De terminalsessie is verbonden.", + "testNotConnected": "Er is geen verbonden terminalsessie met deze instantie-ID.", + "testSelectedMode": "Een upload zou het volgende gebruiken: {{mode}}", + "testLocalVisible": "De lokale map is zichtbaar vanaf de verbonden host.", + "testLocalNotVisible": "De lokale map is NIET zichtbaar vanaf de verbonden host." + }, + "settings": { + "admin": { + "group": { + "touch": "Aanraakinvoer", + "images": "Afbeeldingsopslag" + }, + "sessionTimeoutMinutes": { + "label": "Terminalsessiepersistentie", + "description": "Hoe lang een losgekoppelde terminal actief blijft op de server voordat deze wordt gesloten. Min. 1 · Max. 1440 minuten. Voor sessies die langer dan deze tijd actief moeten blijven, schakelt u Auto-Tmux in op de host." + }, + "commandHistoryEnabled": { + "label": "Commandogeschiedenis", + "description": "Sta het vastleggen van de opdrachtgeschiedenis toe. Indien uitgeschakeld, wordt de geschiedenis niet opgeslagen, ongeacht de instellingen per host." + }, + "imageStorageMode": { + "label": "Opslagmodus", + "description": "Hier worden geplakte en geüploade terminalafbeeldingen opgeslagen. Automatische selectie op basis van de verbinding, lokaal en extern (SFTP) zijn vastgelegd.", + "auto": "Auto", + "local": "Lokaal", + "remoteSftp": "Op afstand (SFTP)" + }, + "imageLocalDir": { + "label": "Lokale map" + }, + "imageHostPath": { + "label": "Hostpad", + "description": "De weergave aan de hostzijde van de lokale directory die terminalagents ontvangen." + }, + "imageTtlMs": { + "label": "TTL (ms)" + }, + "imageMaxCount": { + "label": "Maximaal aantal afbeeldingen" + }, + "imageMaxBytes": { + "label": "Maximale opslagcapaciteit in bytes" + } + }, + "host": { + "enableTerminalToolbar": { + "label": "Terminalwerkbalk inschakelen", + "description": "Toon een sneltoegangsbalk die is vastgezet aan de actieve terminal voor de tools en live statistieken van deze host." + }, + "enableCommandHistory": { + "label": "Commandogeschiedenis", + "description": "Sla de commando's die in deze terminal worden uitgevoerd op voor de geschiedenis en automatisch aanvullen." + }, + "enableTerminal": { + "label": "Terminal inschakelen" + } + } + }, + "palette": { + "localTerminal": "Lokale terminal" + } +} diff --git a/plugins/ssh-terminal/locales/translated/no_NO.json b/plugins/ssh-terminal/locales/translated/no_NO.json new file mode 100644 index 000000000..4477569d4 --- /dev/null +++ b/plugins/ssh-terminal/locales/translated/no_NO.json @@ -0,0 +1,203 @@ +{ + "mobileKeyboard": { + "addQuickKey": "Legge til", + "arrowDown": "Pil ned", + "arrowLeft": "Pil venstre", + "arrowRight": "Pil høyre", + "arrowUp": "Pil opp", + "backTab": "⇥", + "ctrl": "Ctrl", + "delete": "Del", + "done": "Ferdig", + "editQuickKeys": "Rediger hurtigtaster", + "end": "Slutt", + "esc": "Esc", + "home": "Hjem", + "pageDown": "PgDn", + "pageUp": "PgUp", + "paste": "Lim inn", + "quickKeyPlaceholder": "f.eks. sudo ", + "quickKeysDesc": "Trykk på × for å fjerne. Støtter opptil 8 tegn.", + "quickKeysTitle": "Hurtigtaster", + "removeQuickKey": "Fjerne", + "resetDefaults": "Tilbakestill til standardinnstillinger", + "shift": "Skifte", + "tab": "Fane" + }, + "terminal": { + "autoTmuxEnableFailed": "Kunne ikke aktivere Auto-Tmux", + "autoTmuxEnabled": "Auto-Tmux er aktivert for {{host}}. Det gjelder fra neste tilkobling.", + "clipboardHttpWarning": "Kunne ikke lese utklippstavlen. Noen nettlesere begrenser tilgang til utklippstavlen utenfor HTTPS – prøv Ctrl+V i stedet, eller server Termix over HTTPS.", + "closeTab": "Lukke", + "connecting": "Kobler til...", + "connectionLost": "Forbindelsen gikk tapt", + "connectionRejected": "Tilkoblingen ble avvist av serveren. Sjekk autentiseringen og nettverkskonfigurasjonen din.", + "connectionTimeout": "Tidsavbrudd for tilkobling", + "enableAutoTmuxAction": "Aktiver Auto-Tmux", + "hostKeyRejected": "Verifisering av SSH-vertsnøkkel avvist. Tilkoblingen ble avbrutt.", + "linkDialogCopy": "Kopiere", + "linkDialogOpen": "Åpne", + "linkDialogTitle": "Åpne lenke", + "maxReconnectAttemptsReached": "Maksimalt antall forsøk på å koble til igjen er nådd", + "noHostSelected": "Ingen vert valgt", + "opksshAuthFailed": "Autentiseringen mislyktes. Sjekk legitimasjonen din og prøv på nytt.", + "opksshTimeout": "Autentiseringen ble tidsavbrutt. Prøv på nytt.", + "passwordPromptFillTitle": "Skriv inn det lagrede passordet i denne meldingen?", + "reconnect": "Koble til igjen", + "reconnected": "Tilkoblet på nytt", + "reconnecting": "Kobler til på nytt... ({{attempt}}/{{max}})", + "searchCaseSensitive": "Match-saken", + "searchClose": "Lukk (Escape)", + "searchNext": "Neste kamp (Enter)", + "searchNoResults": "Ingen resultater", + "searchPlaceholder": "Finne", + "searchPrevious": "Forrige treff (Shift+Enter)", + "searchRegex": "Bruk regulært uttrykk", + "searchResultCount": "{{index}} av {{count}}", + "searchWholeWord": "Finn hele ordet", + "sessionExpiredNotice": "Din forrige økt på {{host}} utløp etter {{minutes}} minutter frakobling; skallet og alt som kjører i det er borte. Aktiver Auto-Tmux på denne verten for å holde øktene aktive.", + "sessionExpiredNoticeNoMinutes": "Din forrige økt på {{host}} utløp mens den var frakoblet; skallet og alt som kjører i det er borte. Aktiver Auto-Tmux på denne verten for å holde øktene aktive.", + "sessionTakenOver": "Økten ble åpnet i en annen fane. Kobler til igjen...", + "tmuxAttached": "Tilknyttede klienter", + "tmuxAttachedCount": "{{count}} vedlagt", + "tmuxCopyHint": "Juster valget og trykk Enter for å kopiere til utklippstavlen", + "tmuxCreateNew": "Start ny økt", + "tmuxDetached": "Frakoblet fra tmux-økten", + "tmuxLastActivity": "Siste aktivitet", + "tmuxSessionAttached": "tmux-økt vedlagt: {{name}}", + "tmuxSessionCreated": "tmux-økt opprettet: {{name}}", + "tmuxSessionPickerDesc": "Eksisterende tmux-økter funnet på denne verten. Velg én for å koble til på nytt eller opprett en ny økt.", + "tmuxSessionPickerTitle": "tmux-økter", + "tmuxTimeDays": "for {{count}}dager siden", + "tmuxTimeHours": "for {{count}}t siden", + "tmuxTimeJustNow": "akkurat nå", + "tmuxTimeMinutes": "For {{count}}minutter siden", + "tmuxUnavailable": "tmux er ikke installert på den eksterne verten, og går tilbake til standard skall", + "tmuxWindowCount": "{{count}} vindu", + "tmuxWindows": "Vinduer", + "unknownError": "Ukjent feil oppstod", + "vaultAuthDescription": "Et vindu har åpnet seg for å logge på HashiCorp Vault. Fullfør påloggingen der; denne tilkoblingen vil fortsette automatisk.", + "vaultAuthFailed": "Hvelv-autentisering mislyktes. Prøv på nytt.", + "vaultAuthTitle": "Pålogging til arkivet kreves", + "vaultReopen": "Åpne påloggingsvinduet på nytt", + "websocketAbnormalClose": "Tilkoblingen ble avsluttet uventet. Dette kan skyldes et problem med omvendt proxy eller SSL-konfigurasjon. Sjekk serverloggene.", + "websocketError": "WebSocket-tilkoblingsfeil" + }, + "terminalToolbar": { + "detachTmux": "Koble fra tmux", + "detachTmuxDescription": "Koble fra tmux-økten", + "dismissError": "Avvis bildefeil", + "hideToolbar": "Skjul verktøylinjen", + "image": "Bildehandlinger (kun bilder)", + "imageActionFailed": "Bildehandlingen mislyktes", + "layout": "Visningsmodus for verktøylinjen", + "layoutExpanded": "Utvidet med målinger", + "layoutIcon": "Kun ikon", + "layoutLabeled": "Ikoner og etiketter", + "moveToolbar": "Flytt verktøylinjen", + "moveToolbarHint": "Dra for å flytte verktøylinjen", + "paste": "Lim inn bilde", + "pasteImage": "Lim inn bilde fra utklippstavlen (kun bilder)", + "retry": "Prøv på nytt", + "showToolbar": "Utvid verktøylinjen", + "upload": "Last opp bilde", + "uploadImage": "Last opp et bilde til verten", + "uploadingImage": "Laster opp..." + }, + "history": { + "noTerminalSelected": "Ingen terminal valgt", + "noTerminalSelectedHint": "Åpne en SSH-terminalfane for å se kommandohistorikken", + "searchPlaceholder": "Søkelogg...", + "clearAll": "Fjern alt", + "noHistoryEntries": "Ingen historikkoppføringer", + "trackingDisabled": "Historikksporing er deaktivert" + }, + "touchInput": { + "enabled": "Aktiver berøringsinndata", + "enabledDesc": "Tillat énfinger-berøringsrulling i terminaler", + "momentumEnabled": "Aktiver momentum", + "momentumEnabledDesc": "Fortsett å bla kort etter et sveip", + "advanced": "Avansert", + "resetDefaults": "Tilbakestill til standardinnstillinger", + "dragThreshold": "Dragterskel", + "maxWheelDelta": "Maksimal hjuldelta", + "momentumSampleWindow": "Momentum-eksempelvindu", + "releaseGracePeriod": "Frigivelsesfrist", + "minimumVelocity": "Minimumshastighet", + "maximumVelocity": "Maksimal hastighet", + "maximumDuration": "Maksimal varighet", + "maximumTravel": "Maksimal reise", + "decayTime": "Forfallstid", + "pixelsPerTick": "Piksler per tikk", + "maximumFrameInterval": "Maksimalt bildeintervall", + "maximumTicksPerFrame": "Maksimalt antall ticks per ramme" + }, + "imageStorage": { + "instanceId": "Terminaløktforekomst-ID", + "instanceIdDesc": "Forekomst-ID for en av dine tilkoblede terminalfaner, brukt til å teste synlighet.", + "selectedModeUnavailable": "Utilgjengelig", + "test": "Test", + "testing": "Testing...", + "testFailed": "Kunne ikke teste bildelagring", + "testConnected": "Terminaløkten er tilkoblet.", + "testNotConnected": "Ingen tilkoblet terminaløkt med denne forekomst-ID-en.", + "testSelectedMode": "En opplasting ville bruke: {{mode}}", + "testLocalVisible": "Den lokale katalogen er synlig fra den tilkoblede verten.", + "testLocalNotVisible": "Den lokale katalogen er IKKE synlig fra den tilkoblede verten." + }, + "settings": { + "admin": { + "group": { + "touch": "Berøringsinndata", + "images": "Bildelagring" + }, + "sessionTimeoutMinutes": { + "label": "Terminaløktpersistens", + "description": "Hvor lenge en frakoblet terminal holdes aktiv på serveren før den lukkes. Min 1 · Maks 1440 minutter. For økter som må overleve dette, aktiver Auto-Tmux på verten." + }, + "commandHistoryEnabled": { + "label": "Kommandohistorikk", + "description": "Tillat opptak av kommandohistorikk. Når den er deaktivert, lagres ikke historikken uavhengig av innstillinger per vert." + }, + "imageStorageMode": { + "label": "Lagringsmodus", + "description": "Der innlimte og opplastede terminalbilder skrives. Automatiske valg etter tilkobling, lokal og ekstern (SFTP) er fikset.", + "auto": "Bil", + "local": "Lokalt", + "remoteSftp": "Fjernstyrt (SFTP)" + }, + "imageLocalDir": { + "label": "Lokal katalog" + }, + "imageHostPath": { + "label": "Vertssti", + "description": "Vertsvisningen av den lokale katalogen som terminalagenter mottar." + }, + "imageTtlMs": { + "label": "TTL (ms)" + }, + "imageMaxCount": { + "label": "Maksimalt antall bilder" + }, + "imageMaxBytes": { + "label": "Maksimalt antall lagringsbyte" + } + }, + "host": { + "enableTerminalToolbar": { + "label": "Aktiver terminalverktøylinjen", + "description": "Vis en verktøylinje for hurtig tilgang som er festet til den aktive terminalen for denne vertens verktøy og livestatistikk" + }, + "enableCommandHistory": { + "label": "Kommandohistorikk", + "description": "Record-kommandoer kjøres i denne terminalen for historikk og autofullføring" + }, + "enableTerminal": { + "label": "Aktiver terminalen" + } + } + }, + "palette": { + "localTerminal": "Lokal terminal" + } +} diff --git a/plugins/ssh-terminal/locales/translated/pl_PL.json b/plugins/ssh-terminal/locales/translated/pl_PL.json new file mode 100644 index 000000000..681abbd68 --- /dev/null +++ b/plugins/ssh-terminal/locales/translated/pl_PL.json @@ -0,0 +1,203 @@ +{ + "mobileKeyboard": { + "addQuickKey": "Dodaj", + "arrowDown": "Strzałka w dół", + "arrowLeft": "Strzałka w lewo", + "arrowRight": "Strzałka w prawo", + "arrowUp": "Strzałka w górę", + "backTab": "⇥", + "ctrl": "Ctrl", + "delete": "Del", + "done": "Gotowe", + "editQuickKeys": "Edytuj szybkie klawisze", + "end": "End", + "esc": "Esc", + "home": "Home", + "pageDown": "PgDn", + "pageUp": "PgUp", + "paste": "Wklej", + "quickKeyPlaceholder": "np. sudo ", + "quickKeysDesc": "Dotknij ×, aby usunąć. Maksymalnie 8 znaków.", + "quickKeysTitle": "Szybkie klawisze", + "removeQuickKey": "Usuń", + "resetDefaults": "Przywróć ustawienia domyślne", + "shift": "Shift", + "tab": "Tab" + }, + "terminal": { + "autoTmuxEnableFailed": "Nie udało się włączyć funkcji Auto-Tmux", + "autoTmuxEnabled": "Auto-Tmux włączony dla {{host}}. Ma zastosowanie od następnego połączenia.", + "clipboardHttpWarning": "Nie udało się odczytać schowka. Niektóre przeglądarki ograniczają dostęp do schowka poza HTTPS. Użyj Ctrl+V albo udostępniaj Termix przez HTTPS.", + "closeTab": "Zamknij", + "connecting": "Łączenie...", + "connectionLost": "Utracono połączenie", + "connectionRejected": "Połączenie odrzucone przez serwer. Sprawdź uwierzytelnianie i konfigurację sieci.", + "connectionTimeout": "Przekroczono limit czasu połączenia", + "enableAutoTmuxAction": "Włącz Auto-Tmux", + "hostKeyRejected": "Weryfikacja klucza hosta SSH odrzucona. Połączenie anulowane.", + "linkDialogCopy": "Kopiuj", + "linkDialogOpen": "Otwórz", + "linkDialogTitle": "Otwórz link", + "maxReconnectAttemptsReached": "Osiągnięto maksymalną liczbę prób ponownego połączenia", + "noHostSelected": "Nie wybrano hosta", + "opksshAuthFailed": "Uwierzytelnienie nie powiodło się. Sprawdź poświadczenia i spróbuj ponownie.", + "opksshTimeout": "Upłynął limit czasu uwierzytelniania. Spróbuj ponownie.", + "passwordPromptFillTitle": "Czy wpisać zapisane hasło w tym monicie?", + "reconnect": "Połącz ponownie", + "reconnected": "Połączono ponownie pomyślnie", + "reconnecting": "Ponowne łączenie... ({{attempt}}/{{max}})", + "searchCaseSensitive": "Uwzględnij wielkość liter", + "searchClose": "Zamknij (Esc)", + "searchNext": "Następne dopasowanie (Enter)", + "searchNoResults": "Brak wyników", + "searchPlaceholder": "Znajdź", + "searchPrevious": "Poprzednie dopasowanie (Shift+Enter)", + "searchRegex": "Użyj wyrażenia regularnego", + "searchResultCount": "{{index}} z {{count}}", + "searchWholeWord": "Dopasuj całe słowo", + "sessionExpiredNotice": "Twoja poprzednia sesja z {{host}} wygasła po {{minutes}} minutach odłączenia. Powłoka i wszystko, co w niej działa, zniknęło. Włącz Auto-Tmux na tym hoście, aby sesje pozostały aktywne.", + "sessionExpiredNoticeNoMinutes": "Twoja poprzednia sesja na {{host}} wygasła podczas rozłączenia; powłoka i wszystko, co w niej działa, zniknęło. Włącz Auto-Tmux na tym hoście, aby sesje pozostały aktywne.", + "sessionTakenOver": "Sesja została otwarta w innej karcie. Ponowne łączenie...", + "tmuxAttached": "Dołączeni klienci", + "tmuxAttachedCount": "{{count}} dołączonych", + "tmuxCopyHint": "Dostosuj zaznaczenie i naciśnij Enter, aby skopiować do schowka", + "tmuxCreateNew": "Rozpocznij nową sesję", + "tmuxDetached": "Odłączono od sesji tmux", + "tmuxLastActivity": "Ostatnia aktywność", + "tmuxSessionAttached": "Dołączono do sesji tmux: {{name}}", + "tmuxSessionCreated": "Utworzono sesję tmux: {{name}}", + "tmuxSessionPickerDesc": "Znaleziono istniejące sesje tmux na tym hoście. Wybierz jedną, aby ponownie dołączyć lub utworzyć nową sesję.", + "tmuxSessionPickerTitle": "Sesje tmux", + "tmuxTimeDays": "{{count}} dni temu", + "tmuxTimeHours": "{{count}} godz. temu", + "tmuxTimeJustNow": "przed chwilą", + "tmuxTimeMinutes": "{{count}} min temu", + "tmuxUnavailable": "tmux nie jest zainstalowany na zdalnym hoście, następuje powrót do standardowej powłoki", + "tmuxWindowCount": "{{count}} okno", + "tmuxWindows": "Okna", + "unknownError": "Wystąpił nieznany błąd", + "vaultAuthDescription": "Otworzyło się okno logowania do HashiCorp Vault. Dokończ logowanie, a połączenie zostanie nawiązane automatycznie.", + "vaultAuthFailed": "Uwierzytelnienie w Vault nie powiodło się. Spróbuj ponownie.", + "vaultAuthTitle": "Wymagane logowanie do Vault", + "vaultReopen": "Otwórz ponownie okno logowania", + "websocketAbnormalClose": "Połączenie zostało nieoczekiwanie zamknięte. Przyczyną może być reverse proxy lub problem z konfiguracją SSL. Sprawdź logi serwera.", + "websocketError": "Błąd połączenia WebSocket" + }, + "terminalToolbar": { + "detachTmux": "Odłącz tmux", + "detachTmuxDescription": "Odłącz sesję tmux", + "dismissError": "Odrzuć błąd obrazu", + "hideToolbar": "Ukryj pasek narzędzi", + "image": "Akcje obrazu (tylko obrazy)", + "imageActionFailed": "Akcja obrazu nie powiodła się", + "layout": "Tryb wyświetlania paska narzędzi", + "layoutExpanded": "Rozszerzone o metryki", + "layoutIcon": "Tylko ikona", + "layoutLabeled": "Ikony i etykiety", + "moveToolbar": "Przenieś pasek narzędzi", + "moveToolbarHint": "Przeciągnij, aby zmienić położenie paska narzędzi", + "paste": "Wklej obraz", + "pasteImage": "Wklej obraz ze schowka (tylko obrazy)", + "retry": "Ponów", + "showToolbar": "Rozwiń pasek narzędzi", + "upload": "Prześlij obraz", + "uploadImage": "Prześlij obraz do hosta", + "uploadingImage": "Przesyłanie..." + }, + "history": { + "noTerminalSelected": "Nie wybrano terminala", + "noTerminalSelectedHint": "Otwórz kartę terminala SSH, aby wyświetlić historię poleceń", + "searchPlaceholder": "Przeszukaj historię...", + "clearAll": "Wyczyść wszystko", + "noHistoryEntries": "Brak wpisów w historii", + "trackingDisabled": "Śledzenie historii jest wyłączone" + }, + "touchInput": { + "enabled": "Włącz wprowadzanie dotykowe", + "enabledDesc": "Zezwól na przewijanie jednym palcem w terminalach", + "momentumEnabled": "Włącz Momentum", + "momentumEnabledDesc": "Kontynuuj przewijanie przez chwilę po przesunięciu", + "advanced": "Zaawansowane", + "resetDefaults": "Przywróć ustawienia domyślne", + "dragThreshold": "Próg przeciągania", + "maxWheelDelta": "Maksymalna delta koła", + "momentumSampleWindow": "Okno próbki pędu", + "releaseGracePeriod": "Okres karencji po puszczeniu", + "minimumVelocity": "Minimalna prędkość", + "maximumVelocity": "Maksymalna prędkość", + "maximumDuration": "Maksymalny czas trwania", + "maximumTravel": "Maksymalny dystans", + "decayTime": "Czas wygaszania", + "pixelsPerTick": "Pikseli na tick", + "maximumFrameInterval": "Maksymalny interwał klatek", + "maximumTicksPerFrame": "Maksymalna liczba tików na klatkę" + }, + "imageStorage": { + "instanceId": "Identyfikator instancji sesji terminalowej", + "instanceIdDesc": "Identyfikator instancji jednej z podłączonych kart terminala, używany do testowania widoczności.", + "selectedModeUnavailable": "Niedostępny", + "test": "Test", + "testing": "Testowanie...", + "testFailed": "Nie udało się przetestować magazynu obrazów", + "testConnected": "Sesja terminalowa jest połączona.", + "testNotConnected": "Brak połączonej sesji terminala z tym identyfikatorem instancji.", + "testSelectedMode": "Przesyłanie użyłoby: {{mode}}", + "testLocalVisible": "Katalog lokalny jest widoczny z podłączonego hosta.", + "testLocalNotVisible": "Katalog lokalny NIE jest widoczny z podłączonego hosta." + }, + "settings": { + "admin": { + "group": { + "touch": "Wprowadzanie dotykowe", + "images": "Przechowywanie obrazów" + }, + "sessionTimeoutMinutes": { + "label": "Trwałość sesji terminalowej", + "description": "Jak długo odłączony terminal jest utrzymywany aktywny na serwerze przed jego zamknięciem. Min. 1 · Maks. 1440 minut. W przypadku sesji, które muszą przetrwać ten czas, włącz funkcję Auto-Tmux na hoście." + }, + "commandHistoryEnabled": { + "label": "Historia poleceń", + "description": "Zezwól na rejestrowanie historii poleceń. Po wyłączeniu historia nie jest zapisywana, niezależnie od ustawień poszczególnych hostów." + }, + "imageStorageMode": { + "label": "Tryb przechowywania", + "description": "Gdzie zapisywane są wklejone i przesłane obrazy z terminala. Auto wybiera według połączenia; Lokalny i Zdalny (SFTP) są stałe.", + "auto": "Auto", + "local": "Lokalny", + "remoteSftp": "Zdalny (SFTP)" + }, + "imageLocalDir": { + "label": "Katalog lokalny" + }, + "imageHostPath": { + "label": "Ścieżka hosta", + "description": "Widok katalogu lokalnego od strony hosta, który otrzymują agenci terminalowi." + }, + "imageTtlMs": { + "label": "Czas życia (ms)" + }, + "imageMaxCount": { + "label": "Maksymalna liczba obrazów" + }, + "imageMaxBytes": { + "label": "Maksymalny rozmiar magazynu (bajty)" + } + }, + "host": { + "enableTerminalToolbar": { + "label": "Włącz pasek narzędzi terminala", + "description": "Pokaż pasek narzędzi szybkiego dostępu zadokowany do aktywnego terminala, umożliwiający dostęp do narzędzi i statystyk na żywo tego hosta" + }, + "enableCommandHistory": { + "label": "Historia poleceń", + "description": "Zapisuj polecenia uruchamiane w tym terminalu na potrzeby historii i autouzupełniania" + }, + "enableTerminal": { + "label": "Włącz terminal" + } + } + }, + "palette": { + "localTerminal": "Terminal lokalny" + } +} diff --git a/plugins/ssh-terminal/locales/translated/pt_BR.json b/plugins/ssh-terminal/locales/translated/pt_BR.json new file mode 100644 index 000000000..1b8c1a56a --- /dev/null +++ b/plugins/ssh-terminal/locales/translated/pt_BR.json @@ -0,0 +1,203 @@ +{ + "mobileKeyboard": { + "addQuickKey": "Adicionar", + "arrowDown": "Seta para baixo", + "arrowLeft": "Seta para a esquerda", + "arrowRight": "Seta para a direita", + "arrowUp": "Seta para cima", + "backTab": "⇥", + "ctrl": "Ctrl", + "delete": "Del", + "done": "Feito", + "editQuickKeys": "Editar teclas de atalho", + "end": "Fim", + "esc": "Esc", + "home": "Lar", + "pageDown": "PgDn", + "pageUp": "PgUp", + "paste": "Colar", + "quickKeyPlaceholder": "por exemplo sudo ", + "quickKeysDesc": "Toque em × para remover. Suporta até 8 caracteres.", + "quickKeysTitle": "Teclas de atalho", + "removeQuickKey": "Remover", + "resetDefaults": "Restaurar padrões de fábrica", + "shift": "Mudança", + "tab": "Aba" + }, + "terminal": { + "autoTmuxEnableFailed": "Falha ao ativar o Auto-Tmux", + "autoTmuxEnabled": "Auto-Tmux ativado para {{host}}. Aplica-se a partir da próxima conexão.", + "clipboardHttpWarning": "Não foi possível ler a área de transferência. Alguns navegadores restringem o acesso à área de transferência fora de HTTPS - tente Ctrl+V ou utilize HTTPS para acessar o Termix.", + "closeTab": "Fechar", + "connecting": "Conectando...", + "connectionLost": "Conexão perdida", + "connectionRejected": "Conexão rejeitada pelo servidor. Verifique sua autenticação e configuração de rede.", + "connectionTimeout": "Tempo limite de conexão", + "enableAutoTmuxAction": "Ativar Auto-Tmux", + "hostKeyRejected": "Verificação da chave SSH do host rejeitada. Conexão cancelada.", + "linkDialogCopy": "Cópia", + "linkDialogOpen": "Abrir", + "linkDialogTitle": "Abrir link", + "maxReconnectAttemptsReached": "Número máximo de tentativas de reconexão atingido", + "noHostSelected": "Nenhum host selecionado", + "opksshAuthFailed": "A autenticação falhou. Verifique suas credenciais e tente novamente.", + "opksshTimeout": "A autenticação expirou. Tente novamente.", + "passwordPromptFillTitle": "Digite a senha salva neste campo?", + "reconnect": "Reconectar", + "reconnected": "Reconectado com sucesso", + "reconnecting": "Reconectando... ({{attempt}}/{{max}})", + "searchCaseSensitive": "Caixa de fósforos", + "searchClose": "Fechar (Esc)", + "searchNext": "Próxima partida (Entrar)", + "searchNoResults": "Nenhum resultado", + "searchPlaceholder": "Encontrar", + "searchPrevious": "Partida anterior (Shift+Enter)", + "searchRegex": "Utilizar expressão regular", + "searchResultCount": "{{index}} de {{count}}", + "searchWholeWord": "Combine a palavra inteira", + "sessionExpiredNotice": "Sua sessão anterior em {{host}} expirou após {{minutes}} minutos desconectado; o shell e tudo o que estava sendo executado nele foram perdidos. Habilite o Auto-Tmux neste host para manter as sessões ativas.", + "sessionExpiredNoticeNoMinutes": "Sua sessão anterior em {{host}} expirou enquanto estava desconectada; o shell e tudo o que estava sendo executado nele foram perdidos. Habilite o Auto-Tmux neste host para manter as sessões ativas.", + "sessionTakenOver": "A sessão foi aberta em outra aba. Reconectando...", + "tmuxAttached": "Clientes vinculados", + "tmuxAttachedCount": "{{count}} anexado", + "tmuxCopyHint": "Ajuste a seleção e pressione Enter para copiar para a área de transferência.", + "tmuxCreateNew": "Iniciar nova sessão", + "tmuxDetached": "Sessão desconectada do tmux", + "tmuxLastActivity": "Última atividade", + "tmuxSessionAttached": "Sessão tmux anexada: {{name}}", + "tmuxSessionCreated": "Sessão tmux criada: {{name}}", + "tmuxSessionPickerDesc": "Foram encontradas sessões tmux existentes neste host. Selecione uma para reconectar ou crie uma nova sessão.", + "tmuxSessionPickerTitle": "Sessões tmux", + "tmuxTimeDays": "{{count}}há alguns dias", + "tmuxTimeHours": "{{count}}h atrás", + "tmuxTimeJustNow": "agora mesmo", + "tmuxTimeMinutes": "{{count}}m atrás", + "tmuxUnavailable": "O tmux não está instalado no host remoto, utilizando o shell padrão como alternativa.", + "tmuxWindowCount": "{{count}} janela", + "tmuxWindows": "Windows", + "unknownError": "Ocorreu um erro desconhecido.", + "vaultAuthDescription": "Uma janela foi aberta para fazer login no HashiCorp Vault. Conclua o login; esta conexão será estabelecida automaticamente.", + "vaultAuthFailed": "A autenticação do cofre falhou. Tente novamente.", + "vaultAuthTitle": "É necessário fazer login no cofre.", + "vaultReopen": "Reabrir janela de login", + "websocketAbnormalClose": "A conexão foi encerrada inesperadamente. Isso pode ter ocorrido devido a um problema de configuração de proxy reverso ou SSL. Verifique os registros do servidor.", + "websocketError": "Erro de conexão WebSocket" + }, + "terminalToolbar": { + "detachTmux": "Desanexar tmux", + "detachTmuxDescription": "Desanexar sessão tmux", + "dismissError": "Ignorar erro de imagem", + "hideToolbar": "Ocultar barra de ferramentas", + "image": "Ações de imagem (somente imagens)", + "imageActionFailed": "A ação de imagem falhou", + "layout": "Modo de exibição da barra de ferramentas", + "layoutExpanded": "Expandido com métricas", + "layoutIcon": "Apenas o ícone", + "layoutLabeled": "Ícones e rótulos", + "moveToolbar": "Mover barra de ferramentas", + "moveToolbarHint": "Arraste para reposicionar a barra de ferramentas.", + "paste": "Colar imagem", + "pasteImage": "Colar imagem da área de transferência (somente imagens)", + "retry": "Tentar novamente", + "showToolbar": "Expandir barra de ferramentas", + "upload": "Carregar imagem", + "uploadImage": "Faça o upload de uma imagem para o host.", + "uploadingImage": "Carregando..." + }, + "history": { + "noTerminalSelected": "Nenhum terminal selecionado", + "noTerminalSelectedHint": "Abra uma aba do terminal SSH para visualizar seu histórico de comandos.", + "searchPlaceholder": "Histórico de pesquisa...", + "clearAll": "Limpar tudo", + "noHistoryEntries": "Nenhuma entrada no histórico.", + "trackingDisabled": "O rastreamento do histórico está desativado." + }, + "touchInput": { + "enabled": "Ativar entrada por toque", + "enabledDesc": "Permitir rolagem com um único dedo em terminais.", + "momentumEnabled": "Ativar o impulso", + "momentumEnabledDesc": "Continue rolando brevemente após deslizar o dedo.", + "advanced": "Avançado", + "resetDefaults": "Restaurar padrões de fábrica", + "dragThreshold": "Limiar de arrasto", + "maxWheelDelta": "Delta máximo da roda", + "momentumSampleWindow": "Janela de amostra de momento", + "releaseGracePeriod": "Período de carência para lançamento", + "minimumVelocity": "Velocidade mínima", + "maximumVelocity": "Velocidade máxima", + "maximumDuration": "Duração máxima", + "maximumTravel": "Viagem máxima", + "decayTime": "Tempo de decaimento", + "pixelsPerTick": "Pixels por tick", + "maximumFrameInterval": "Intervalo máximo entre quadros", + "maximumTicksPerFrame": "Número máximo de ticks por quadro" + }, + "imageStorage": { + "instanceId": "ID da instância da sessão do terminal", + "instanceIdDesc": "ID da instância de uma das suas abas de terminal conectadas, usada para testar a visibilidade.", + "selectedModeUnavailable": "Indisponível", + "test": "Teste", + "testing": "Testando...", + "testFailed": "Falha ao testar o armazenamento de imagens", + "testConnected": "A sessão do terminal está conectada.", + "testNotConnected": "Nenhuma sessão de terminal conectada com este ID de instância.", + "testSelectedMode": "Um upload usaria: {{mode}}", + "testLocalVisible": "O diretório local está visível a partir do host conectado.", + "testLocalNotVisible": "O diretório local NÃO está visível a partir do host conectado." + }, + "settings": { + "admin": { + "group": { + "touch": "Entrada por toque", + "images": "Armazenamento de imagens" + }, + "sessionTimeoutMinutes": { + "label": "Persistência de sessão do terminal", + "description": "Tempo que um terminal desconectado permanece ativo no servidor antes de ser fechado. Mínimo 1 minuto · Máximo 1440 minutos. Para sessões que precisam durar mais do que isso, habilite o Auto-Tmux no host." + }, + "commandHistoryEnabled": { + "label": "Histórico de comandos", + "description": "Permitir o registro do histórico de comandos. Quando desativado, o histórico não é salvo, independentemente das configurações de cada host." + }, + "imageStorageMode": { + "label": "Modo de armazenamento", + "description": "Local onde as imagens de terminal coladas e carregadas são exibidas. As seleções automáticas por conexão, Local e Remota (SFTP), foram corrigidas.", + "auto": "Auto", + "local": "Local", + "remoteSftp": "Remoto (SFTP)" + }, + "imageLocalDir": { + "label": "Diretório local" + }, + "imageHostPath": { + "label": "Caminho do host", + "description": "Visão do diretório local recebida pelos agentes de terminal, do lado do host." + }, + "imageTtlMs": { + "label": "TTL (ms)" + }, + "imageMaxCount": { + "label": "Contagem máxima de imagens" + }, + "imageMaxBytes": { + "label": "Bytes de armazenamento máximo" + } + }, + "host": { + "enableTerminalToolbar": { + "label": "Ativar a barra de ferramentas do terminal", + "description": "Exibir uma barra de ferramentas de acesso rápido acoplada ao terminal ativo com as ferramentas e estatísticas em tempo real deste host." + }, + "enableCommandHistory": { + "label": "Histórico de comandos", + "description": "Registre os comandos executados neste terminal para histórico e preenchimento automático." + }, + "enableTerminal": { + "label": "Habilitar terminal" + } + } + }, + "palette": { + "localTerminal": "Terminal local" + } +} diff --git a/plugins/ssh-terminal/locales/translated/pt_PT.json b/plugins/ssh-terminal/locales/translated/pt_PT.json new file mode 100644 index 000000000..7be1ab9b2 --- /dev/null +++ b/plugins/ssh-terminal/locales/translated/pt_PT.json @@ -0,0 +1,203 @@ +{ + "mobileKeyboard": { + "addQuickKey": "Adicionar", + "arrowDown": "Seta para Baixo", + "arrowLeft": "Seta para a Esquerda", + "arrowRight": "Seta para a Direita", + "arrowUp": "Seta para Cima", + "backTab": "⇥", + "ctrl": "Ctrl", + "delete": "Del", + "done": "Concluir", + "editQuickKeys": "Editar teclas rápidas", + "end": "Fim", + "esc": "Esc", + "home": "Início", + "pageDown": "PgDn", + "pageUp": "PgUp", + "paste": "Colar", + "quickKeyPlaceholder": "ex: sudo ", + "quickKeysDesc": "Toque no × para remover. Suporta até 8 caracteres.", + "quickKeysTitle": "Teclas Rápidas", + "removeQuickKey": "Remover", + "resetDefaults": "Restaurar predefinições", + "shift": "Shift", + "tab": "Tab" + }, + "terminal": { + "autoTmuxEnableFailed": "Falha ao ativar o Auto-Tmux", + "autoTmuxEnabled": "Auto-Tmux ativado para {{host}}. Aplica-se a partir da ligação seguinte.", + "clipboardHttpWarning": "Não foi possível ler a área de transferência. Alguns browsers restringem o acesso à área de transferência fora de HTTPS - experimente Ctrl+V ou utilize HTTPS para aceder ao Termix.", + "closeTab": "Fechar", + "connecting": "A ligar...", + "connectionLost": "Ligação perdida", + "connectionRejected": "Ligação rejeitada pelo servidor. Verifique a sua autenticação e configuração de rede.", + "connectionTimeout": "Tempo limite da ligação", + "enableAutoTmuxAction": "Ativar o Auto-Tmux", + "hostKeyRejected": "Verificação da chave de host SSH rejeitada. Ligação cancelada.", + "linkDialogCopy": "Copiar", + "linkDialogOpen": "Abrir", + "linkDialogTitle": "Abrir Ligação", + "maxReconnectAttemptsReached": "Número máximo de tentativas de restabelecimento de ligação atingido", + "noHostSelected": "Nenhum host selecionado", + "opksshAuthFailed": "A autenticação falhou. Verifique as suas credenciais e tente novamente.", + "opksshTimeout": "A autenticação expirou. Tente novamente.", + "passwordPromptFillTitle": "Preencher a palavra-passe guardada neste pedido?", + "reconnect": "Reconectar", + "reconnected": "Ligação restabelecida com sucesso", + "reconnecting": "A restabelecer ligação... ({{attempt}}/{{max}})", + "searchCaseSensitive": "Caixa de fósforos", + "searchClose": "Fechar (Esc)", + "searchNext": "Próxima partida (Entrar)", + "searchNoResults": "Nenhum resultado", + "searchPlaceholder": "Encontrar", + "searchPrevious": "Partida anterior (Shift+Enter)", + "searchRegex": "Utilizar expressão regular", + "searchResultCount": "{{index}} de {{count}}", + "searchWholeWord": "Combine a palavra inteira", + "sessionExpiredNotice": "A sua sessão anterior em {{host}} expirou após {{minutes}} minutos desligado; o shell e tudo o que estava a ser executado nele foram perdidos. Active o Auto-Tmux neste host para manter as sessões ativas.", + "sessionExpiredNoticeNoMinutes": "A sua sessão anterior em {{host}} expirou enquanto estava desligada; o shell e tudo o que estava a ser executado nele foram perdidos. Active o Auto-Tmux neste host para manter as sessões ativas.", + "sessionTakenOver": "A sessão foi aberta noutro separador. A restabelecer ligação...", + "tmuxAttached": "Clientes anexados", + "tmuxAttachedCount": "{{count}} anexado", + "tmuxCopyHint": "Ajuste a seleção e prima Enter para copiar para a área de transferência", + "tmuxCreateNew": "Iniciar nova sessão", + "tmuxDetached": "Desanexado da sessão tmux", + "tmuxLastActivity": "Última atividade", + "tmuxSessionAttached": "Sessão tmux anexada: {{name}}", + "tmuxSessionCreated": "Sessão tmux criada: {{name}}", + "tmuxSessionPickerDesc": "Foram encontradas sessões tmux neste host. Selecione uma para retomar ou crie uma nova sessão.", + "tmuxSessionPickerTitle": "Sessões tmux", + "tmuxTimeDays": "há {{count}} d", + "tmuxTimeHours": "há {{count}} h", + "tmuxTimeJustNow": "agora mesmo", + "tmuxTimeMinutes": "há {{count}} min", + "tmuxUnavailable": "O tmux não está instalado no host remoto, a reverter para shell padrão", + "tmuxWindowCount": "{{count}} janela", + "tmuxWindows": "Janelas", + "unknownError": "Ocorreu um erro desconhecido.", + "vaultAuthDescription": "Foi aberta uma janela para iniciar sessão no HashiCorp Vault. Conclua o início de sessão aí; esta ligação continuará automaticamente.", + "vaultAuthFailed": "A autenticação no Vault falhou. Tente novamente.", + "vaultAuthTitle": "Início de sessão no Vault necessário", + "vaultReopen": "Reabrir janela de início de sessão", + "websocketAbnormalClose": "A ligação foi fechada inesperadamente. Isto pode dever-se a um problema de configuração do proxy reverso ou SSL. Verifique os registos do servidor.", + "websocketError": "Erro de ligação WebSocket" + }, + "terminalToolbar": { + "detachTmux": "Desanexar tmux", + "detachTmuxDescription": "Desanexar sessão tmux", + "dismissError": "Ignorar erro de imagem", + "hideToolbar": "Ocultar barra de ferramentas", + "image": "Ações de imagem (apenas imagens)", + "imageActionFailed": "A ação de imagem falhou", + "layout": "Modo de visualização da barra de ferramentas", + "layoutExpanded": "Expandido com métricas", + "layoutIcon": "Apenas o ícone", + "layoutLabeled": "Ícones e rótulos", + "moveToolbar": "Mover barra de ferramentas", + "moveToolbarHint": "Arraste para reposicionar a barra de ferramentas.", + "paste": "Colar imagem", + "pasteImage": "Colar imagem da área de transferência (apenas imagens)", + "retry": "Tentar novamente", + "showToolbar": "Expandir barra de ferramentas", + "upload": "Carregar imagem", + "uploadImage": "Faça o upload de uma imagem para o host.", + "uploadingImage": "Carregando..." + }, + "history": { + "noTerminalSelected": "Nenhum terminal selecionado", + "noTerminalSelectedHint": "Abra um separador de terminal SSH para ver o histórico de comandos", + "searchPlaceholder": "Pesquisar histórico...", + "clearAll": "Limpar Tudo", + "noHistoryEntries": "Sem entradas no histórico", + "trackingDisabled": "O rastreio do histórico está desativado" + }, + "touchInput": { + "enabled": "Ativar entrada por toque", + "enabledDesc": "Permitir a rolagem com um único dedo nos terminais.", + "momentumEnabled": "Ativar o impulso", + "momentumEnabledDesc": "Continue a rolar brevemente após deslizar o dedo.", + "advanced": "Avançado", + "resetDefaults": "Restaurar as predefinições de fábrica", + "dragThreshold": "Limiar de arrasto", + "maxWheelDelta": "Delta máximo da roda", + "momentumSampleWindow": "Janela de amostra de momento", + "releaseGracePeriod": "Período de carência para lançamento", + "minimumVelocity": "Velocidade mínima", + "maximumVelocity": "Velocidade máxima", + "maximumDuration": "Duração máxima", + "maximumTravel": "Viagem máxima", + "decayTime": "Tempo de decaimento", + "pixelsPerTick": "Pixels por tick", + "maximumFrameInterval": "Intervalo máximo entre fotogramas", + "maximumTicksPerFrame": "Número máximo de ticks por frame" + }, + "imageStorage": { + "instanceId": "ID da instância da sessão do terminal", + "instanceIdDesc": "ID da instância de uma das suas abas de terminal ligadas, utilizada para testar a visibilidade.", + "selectedModeUnavailable": "Indisponível", + "test": "Teste", + "testing": "Testando...", + "testFailed": "Falha ao testar o armazenamento de imagens", + "testConnected": "A sessão do terminal está ligada.", + "testNotConnected": "Nenhuma sessão de terminal ligada com este ID de instância.", + "testSelectedMode": "Um upload utilizaria: {{mode}}", + "testLocalVisible": "O diretório local está visível a partir do host ligado.", + "testLocalNotVisible": "O diretório local NÃO está visível a partir do host ligado." + }, + "settings": { + "admin": { + "group": { + "touch": "Entrada por toque", + "images": "Armazenamento de imagens" + }, + "sessionTimeoutMinutes": { + "label": "Persistência de sessão do terminal", + "description": "Tempo que um terminal desligado permanece ativo no servidor antes de ser fechado. Mínimo 1 minuto · Máximo 1440 minutos. Para sessões que necessitem de durar mais do que isso, ative o Auto-Tmux no host." + }, + "commandHistoryEnabled": { + "label": "Histórico de Comandos", + "description": "Permitir o registo do histórico de comandos. Quando desativado, o histórico não é guardado independentemente das definições por máquina." + }, + "imageStorageMode": { + "label": "Modo de armazenamento", + "description": "Local onde são apresentadas as imagens de terminal coladas e carregadas. As seleções automáticas por ligação, Local e Remota (SFTP), foram corrigidas.", + "auto": "Auto", + "local": "Local", + "remoteSftp": "Remoto (SFTP)" + }, + "imageLocalDir": { + "label": "Diretório local" + }, + "imageHostPath": { + "label": "Caminho do host", + "description": "Vista do diretório local recebida pelos agentes de terminal, do lado do host." + }, + "imageTtlMs": { + "label": "TTL (ms)" + }, + "imageMaxCount": { + "label": "Contagem máxima de imagens" + }, + "imageMaxBytes": { + "label": "Bytes de armazenamento máximo" + } + }, + "host": { + "enableTerminalToolbar": { + "label": "Ativar a barra de ferramentas do terminal", + "description": "Visualizar uma barra de ferramentas de acesso rápido acoplada ao terminal ativo com as ferramentas e estatísticas em tempo real deste host." + }, + "enableCommandHistory": { + "label": "Histórico de comandos", + "description": "Gravar comandos executados neste terminal para histórico e autocompletar" + }, + "enableTerminal": { + "label": "Ativar Terminal" + } + } + }, + "palette": { + "localTerminal": "Terminal local" + } +} diff --git a/plugins/ssh-terminal/locales/translated/ro_RO.json b/plugins/ssh-terminal/locales/translated/ro_RO.json new file mode 100644 index 000000000..6f0b45195 --- /dev/null +++ b/plugins/ssh-terminal/locales/translated/ro_RO.json @@ -0,0 +1,203 @@ +{ + "mobileKeyboard": { + "addQuickKey": "Adăuga", + "arrowDown": "Săgeată în jos", + "arrowLeft": "Săgeată stânga", + "arrowRight": "Săgeată dreapta", + "arrowUp": "Săgeată în sus", + "backTab": "⇥", + "ctrl": "Ctrl", + "delete": "Șterge", + "done": "Făcut", + "editQuickKeys": "Editați tastele rapide", + "end": "Sfârşit", + "esc": "Esc", + "home": "Acasă", + "pageDown": "Pagină următoare", + "pageUp": "PagUp", + "paste": "Pastă", + "quickKeyPlaceholder": "de exemplu sudo ", + "quickKeysDesc": "Atingeți × pentru a elimina. Acceptă până la 8 caractere.", + "quickKeysTitle": "Taste rapide", + "removeQuickKey": "Elimina", + "resetDefaults": "Resetare la setările implicite", + "shift": "Schimbare", + "tab": "Tab" + }, + "terminal": { + "autoTmuxEnableFailed": "Nu s-a putut activa Auto-Tmux", + "autoTmuxEnabled": "Auto-Tmux activat pentru {{host}}. Se aplică de la următoarea conexiune.", + "clipboardHttpWarning": "Nu s-a putut citi clipboard-ul. Unele browsere restricționează accesul la clipboard în afara HTTPS - încercați Ctrl+V sau serviți Termix prin HTTPS.", + "closeTab": "Aproape", + "connecting": "Se conectează...", + "connectionLost": "Conexiune pierdută", + "connectionRejected": "Conexiune respinsă de server. Vă rugăm să verificați autentificarea și configurația rețelei.", + "connectionTimeout": "Expirare conexiune", + "enableAutoTmuxAction": "Activează Auto-Tmux", + "hostKeyRejected": "Verificarea cheii gazdei SSH a fost respinsă. Conexiunea a fost anulată.", + "linkDialogCopy": "Copie", + "linkDialogOpen": "Deschide", + "linkDialogTitle": "Deschideți linkul", + "maxReconnectAttemptsReached": "S-a atins numărul maxim de încercări de reconectare", + "noHostSelected": "Nicio gazdă selectată", + "opksshAuthFailed": "Autentificarea a eșuat. Verificați acreditările și încercați din nou.", + "opksshTimeout": "Autentificarea a expirat. Vă rugăm să încercați din nou.", + "passwordPromptFillTitle": "Introduceți parola salvată în această solicitare?", + "reconnect": "Reconectați-vă", + "reconnected": "Reconectare cu succes", + "reconnecting": "Se reconecta... ({{attempt}}/{{max}})", + "searchCaseSensitive": "Potrivire caz", + "searchClose": "Închide (Escape)", + "searchNext": "Următorul meci (Introduce)", + "searchNoResults": "Niciun rezultat", + "searchPlaceholder": "Găsi", + "searchPrevious": "Potrivire anterioară (Shift+Enter)", + "searchRegex": "Folosește expresia regulată", + "searchResultCount": "{{index}} din {{count}}", + "searchWholeWord": "Potrivire cuvânt întreg", + "sessionExpiredNotice": "Sesiunea anterioară de pe {{host}} a expirat după {{minutes}} minute de deconectare; shell-ul și tot ce rulează în el au dispărut. Activați Auto-Tmux pe această gazdă pentru a menține sesiunile active.", + "sessionExpiredNoticeNoMinutes": "Sesiunea anterioară pe {{host}} a expirat în timp ce era deconectat; shell-ul și tot ce rulează în el au dispărut. Activați Auto-Tmux pe această gazdă pentru a menține sesiunile active.", + "sessionTakenOver": "Sesiunea a fost deschisă într-o altă filă. Se reconectare...", + "tmuxAttached": "Clienți atașați", + "tmuxAttachedCount": "{{count}} atașat", + "tmuxCopyHint": "Ajustați selecția și apăsați Enter pentru a copia în clipboard", + "tmuxCreateNew": "Începeți o sesiune nouă", + "tmuxDetached": "Detașat de sesiunea tmux", + "tmuxLastActivity": "Ultima activitate", + "tmuxSessionAttached": "sesiune tmux atașată: {{name}}", + "tmuxSessionCreated": "Sesiune tmux creată: {{name}}", + "tmuxSessionPickerDesc": "Au fost găsite sesiuni tmux existente pe această gazdă. Selectați una pentru a o reatașa sau pentru a crea o sesiune nouă.", + "tmuxSessionPickerTitle": "Sesiuni tmux", + "tmuxTimeDays": "{{count}}zile în urmă", + "tmuxTimeHours": "{{count}}acum o oră", + "tmuxTimeJustNow": "chiar acum", + "tmuxTimeMinutes": "{{count}}acum câteva minute", + "tmuxUnavailable": "tmux nu este instalat pe gazda la distanță, revenind la shell-ul standard", + "tmuxWindowCount": "fereastră {{count}}", + "tmuxWindows": "Ferestre", + "unknownError": "A apărut o eroare necunoscută", + "vaultAuthDescription": "S-a deschis o fereastră pentru a vă conecta la HashiCorp Vault. Finalizați conectarea acolo; această conexiune va continua automat.", + "vaultAuthFailed": "Autentificarea în seif a eșuat. Vă rugăm să încercați din nou.", + "vaultAuthTitle": "Este necesară conectarea la seif", + "vaultReopen": "Redeschideți fereastra de conectare", + "websocketAbnormalClose": "Conexiunea s-a închis pe neașteptate. Acest lucru se poate datora unei probleme de configurare a proxy-ului invers sau a SSL. Vă rugăm să verificați jurnalele serverului.", + "websocketError": "Eroare de conexiune WebSocket" + }, + "terminalToolbar": { + "detachTmux": "Detașați tmux", + "detachTmuxDescription": "Detașați sesiunea tmux", + "dismissError": "Eroare de închidere a imaginii", + "hideToolbar": "Ascunde bara de instrumente", + "image": "Acțiuni de imagine (doar imagini)", + "imageActionFailed": "Acțiunea imaginii a eșuat", + "layout": "Mod de afișare a barei de instrumente", + "layoutExpanded": "Extins cu valori metrice", + "layoutIcon": "Numai pictogramă", + "layoutLabeled": "Pictograme și etichete", + "moveToolbar": "Mută bara de instrumente", + "moveToolbarHint": "Trageți pentru a repoziționa bara de instrumente", + "paste": "Lipiți imaginea", + "pasteImage": "Lipiți imaginea din clipboard (doar imagini)", + "retry": "Reîncercați", + "showToolbar": "Extindeți bara de instrumente", + "upload": "Încărcați imaginea", + "uploadImage": "Încărcați o imagine către gazdă", + "uploadingImage": "Se încarcă..." + }, + "history": { + "noTerminalSelected": "Niciun terminal selectat", + "noTerminalSelectedHint": "Deschideți o filă de terminal SSH pentru a vizualiza istoricul comenzilor sale", + "searchPlaceholder": "Istoricul căutărilor...", + "clearAll": "Ștergeți tot", + "noHistoryEntries": "Nicio intrare în istoric", + "trackingDisabled": "Urmărirea istoricului este dezactivată" + }, + "touchInput": { + "enabled": "Activează introducerea tactilă", + "enabledDesc": "Permiteți derularea tactilă cu un singur deget în terminale", + "momentumEnabled": "Activează impulsul", + "momentumEnabledDesc": "Continuați derularea scurtă după o glisare", + "advanced": "Avansat", + "resetDefaults": "Resetare la setările implicite", + "dragThreshold": "Pragul de rezistență", + "maxWheelDelta": "Delta maximă a roții", + "momentumSampleWindow": "Fereastra de eșantionare a impulsului", + "releaseGracePeriod": "Perioada de grație a lansării", + "minimumVelocity": "Viteză minimă", + "maximumVelocity": "Viteză maximă", + "maximumDuration": "Durată maximă", + "maximumTravel": "Cursă maximă", + "decayTime": "Timp de descreștere", + "pixelsPerTick": "Pixeli per bifă", + "maximumFrameInterval": "Interval maxim de cadre", + "maximumTicksPerFrame": "Număr maxim de ticuri per cadru" + }, + "imageStorage": { + "instanceId": "ID-ul instanței sesiunii terminale", + "instanceIdDesc": "ID-ul instanței uneia dintre filele terminalului conectat, folosit pentru a testa vizibilitatea.", + "selectedModeUnavailable": "Indisponibil", + "test": "Test", + "testing": "Testare...", + "testFailed": "Nu s-a putut testa stocarea imaginilor", + "testConnected": "Sesiunea terminalului este conectată.", + "testNotConnected": "Nicio sesiune de terminal conectată cu acest ID de instanță.", + "testSelectedMode": "O încărcare ar folosi: {{mode}}", + "testLocalVisible": "Directorul local este vizibil de pe gazda conectată.", + "testLocalNotVisible": "Directorul local NU este vizibil de pe gazda conectată." + }, + "settings": { + "admin": { + "group": { + "touch": "Intrare tactilă", + "images": "Stocarea imaginilor" + }, + "sessionTimeoutMinutes": { + "label": "Persistența sesiunii terminale", + "description": "Cât timp este menținut activ pe server un terminal deconectat înainte de a fi închis. Min 1 · Max 1440 de minute. Pentru sesiunile care trebuie să dureze mai mult decât această durată, activați Auto-Tmux pe gazdă." + }, + "commandHistoryEnabled": { + "label": "Istoricul comenzilor", + "description": "Permite înregistrarea istoricului comenzilor. Când este dezactivată, istoricul nu este salvat, indiferent de setările pentru fiecare gazdă." + }, + "imageStorageMode": { + "label": "Mod de stocare", + "description": "Unde sunt scrise imaginile terminalului lipite și încărcate. Selecțiile automate prin conexiune, Local și Remote (SFTP) sunt fixe.", + "auto": "Auto", + "local": "Local", + "remoteSftp": "La distanță (SFTP)" + }, + "imageLocalDir": { + "label": "Director local" + }, + "imageHostPath": { + "label": "Calea gazdă", + "description": "Vizualizarea din partea gazdei a directorului local pe care îl primesc agenții terminalului." + }, + "imageTtlMs": { + "label": "TTL (ms)" + }, + "imageMaxCount": { + "label": "Număr maxim de imagini" + }, + "imageMaxBytes": { + "label": "Capacitate maximă de stocare în octeți" + } + }, + "host": { + "enableTerminalToolbar": { + "label": "Activează bara de instrumente Terminal", + "description": "Afișează o bară de instrumente cu acces rapid atașată la terminalul activ pentru instrumentele și statisticile live ale acestei gazde" + }, + "enableCommandHistory": { + "label": "Istoricul comenzilor", + "description": "Înregistrați comenzile executate în acest terminal pentru istoric și completare automată" + }, + "enableTerminal": { + "label": "Activează terminalul" + } + } + }, + "palette": { + "localTerminal": "Terminal local" + } +} diff --git a/plugins/ssh-terminal/locales/translated/ru_RU.json b/plugins/ssh-terminal/locales/translated/ru_RU.json new file mode 100644 index 000000000..0e53456fb --- /dev/null +++ b/plugins/ssh-terminal/locales/translated/ru_RU.json @@ -0,0 +1,203 @@ +{ + "mobileKeyboard": { + "addQuickKey": "Добавить", + "arrowDown": "Вниз", + "arrowLeft": "Влево", + "arrowRight": "Вправо", + "arrowUp": "Вверх", + "backTab": "⇥", + "ctrl": "Ctrl", + "delete": "Del", + "done": "Готово", + "editQuickKeys": "Редактировать", + "end": "End", + "esc": "Esc", + "home": "Home", + "pageDown": "PgDn", + "pageUp": "PgUp", + "paste": "Вставить", + "quickKeyPlaceholder": "напр. sudo ", + "quickKeysDesc": "Нажмите × для удаления. Поддерживается до 8 символов.", + "quickKeysTitle": "Быстрые клавиши", + "removeQuickKey": "Удалить", + "resetDefaults": "Сбросить по умолчанию", + "shift": "Shift", + "tab": "Tab" + }, + "terminal": { + "autoTmuxEnableFailed": "Не удалось включить Auto-Tmux.", + "autoTmuxEnabled": "Автоматический Tmux включен для {{host}}. Он применяется со следующего соединения.", + "clipboardHttpWarning": "Не удалось прочитать содержимое буфера обмена. Некоторые браузеры ограничивают доступ к буферу обмена вне протокола HTTPS — попробуйте использовать Ctrl+V или используйте Termix через HTTPS.", + "closeTab": "Закрыть", + "connecting": "Подключение...", + "connectionLost": "Соединение потеряно", + "connectionRejected": "Подключение отклонено сервером. Проверьте аутентификацию и сетевые настройки.", + "connectionTimeout": "Тайм-аут подключения", + "enableAutoTmuxAction": "Включить Auto-Tmux", + "hostKeyRejected": "Проверка ключа хоста SSH отклонена. Подключение отменено.", + "linkDialogCopy": "Копировать", + "linkDialogOpen": "Открыть", + "linkDialogTitle": "Открыть ссылку", + "maxReconnectAttemptsReached": "Достигнуто максимальное количество попыток переподключения", + "noHostSelected": "Не выбран хост", + "opksshAuthFailed": "Аутентификация не удалась. Проверьте учётные данные и попробуйте снова.", + "opksshTimeout": "Время аутентификации истекло. Попробуйте снова.", + "passwordPromptFillTitle": "Вставить сохраненный пароль в этот запрос?", + "reconnect": "Переподключиться", + "reconnected": "Успешно переподключено", + "reconnecting": "Переподключение... ({{attempt}}/{{max}})", + "searchCaseSensitive": "Спичечный коробок", + "searchClose": "Закрыть (Выход)", + "searchNext": "Следующий матч (Вход)", + "searchNoResults": "Результаты отсутствуют", + "searchPlaceholder": "Находить", + "searchPrevious": "Предыдущий матч (Shift+Enter)", + "searchRegex": "Используйте регулярные выражения", + "searchResultCount": "{{index}} из {{count}}", + "searchWholeWord": "Сопоставьте целое слово", + "sessionExpiredNotice": "Ваша предыдущая сессия на {{host}} завершилась через {{minutes}} минут после разрыва соединения; оболочка и все запущенные в ней программы удалены. Включите Auto-Tmux на этом хосте, чтобы поддерживать сессии активными.", + "sessionExpiredNoticeNoMinutes": "Ваша предыдущая сессия на {{host}} завершилась во время отключения; оболочка и все запущенные в ней приложения удалены. Включите Auto-Tmux на этом хосте, чтобы поддерживать сессии активными.", + "sessionTakenOver": "Сеанс был открыт в другой вкладке. Переподключение...", + "tmuxAttached": "Подключённые клиенты", + "tmuxAttachedCount": "{{count}} подключено", + "tmuxCopyHint": "Скорректируйте выделение и нажмите Enter, чтобы скопировать в буфер обмена", + "tmuxCreateNew": "Начать новую сессию", + "tmuxDetached": "Отключён от сессии tmux", + "tmuxLastActivity": "Последняя активность", + "tmuxSessionAttached": "Сессия tmux подключена: {{name}}", + "tmuxSessionCreated": "Сессия tmux создана: {{name}}", + "tmuxSessionPickerDesc": "Найдены существующие сессии tmux на этом хосте. Выберите одну для переподключения или создайте новую сессию.", + "tmuxSessionPickerTitle": "Сессии tmux", + "tmuxTimeDays": "{{count}}д назад", + "tmuxTimeHours": "{{count}}ч назад", + "tmuxTimeJustNow": "только что", + "tmuxTimeMinutes": "{{count}}м назад", + "tmuxUnavailable": "tmux не установлен на удалённом хосте, переход на стандартную оболочку", + "tmuxWindowCount": "{{count}} окно", + "tmuxWindows": "Окна", + "unknownError": "Произошла неизвестная ошибка", + "vaultAuthDescription": "Открыто окно для входа в HashiCorp Vault. Завершите вход там; подключение продолжится автоматически.", + "vaultAuthFailed": "Аутентификация Vault не удалась. Попробуйте снова.", + "vaultAuthTitle": "Требуется вход в Vault", + "vaultReopen": "Повторно открыть окно входа", + "websocketAbnormalClose": "Соединение неожиданно закрыто. Возможно, проблема с обратным прокси или конфигурацией SSL. Проверьте журналы сервера.", + "websocketError": "Ошибка подключения WebSocket" + }, + "terminalToolbar": { + "detachTmux": "Отсоединить tmux", + "detachTmuxDescription": "Отключить сессию tmux", + "dismissError": "Отклонить ошибку изображения", + "hideToolbar": "Скрыть панель инструментов", + "image": "Действия с изображениями (только с изображениями)", + "imageActionFailed": "Операция с изображением не удалась.", + "layout": "режим отображения панели инструментов", + "layoutExpanded": "Дополнено метриками", + "layoutIcon": "Только иконка", + "layoutLabeled": "Иконки и подписи", + "moveToolbar": "Переместить панель инструментов", + "moveToolbarHint": "Перетащите панель инструментов, чтобы изменить ее положение.", + "paste": "Вставить изображение", + "pasteImage": "Вставить изображение из буфера обмена (только изображения)", + "retry": "Повторить попытку", + "showToolbar": "Развернуть панель инструментов", + "upload": "Загрузить изображение", + "uploadImage": "Загрузите изображение на хостинг.", + "uploadingImage": "Загрузка..." + }, + "history": { + "noTerminalSelected": "Терминал не выбран", + "noTerminalSelectedHint": "Откройте вкладку SSH-терминала, чтобы просмотреть историю команд", + "searchPlaceholder": "Поиск по истории...", + "clearAll": "Очистить всё", + "noHistoryEntries": "Нет записей в истории", + "trackingDisabled": "Отслеживание истории отключено" + }, + "touchInput": { + "enabled": "Включить сенсорный ввод", + "enabledDesc": "Разрешить прокрутку одним пальцем в терминалах", + "momentumEnabled": "Включить импульс", + "momentumEnabledDesc": "После свайпа продолжите прокрутку еще немного.", + "advanced": "Передовой", + "resetDefaults": "Сбросить до значений по умолчанию", + "dragThreshold": "Порог сопротивления", + "maxWheelDelta": "Максимальная разница колес", + "momentumSampleWindow": "Окно выборки импульса", + "releaseGracePeriod": "Льготный период освобождения", + "minimumVelocity": "Минимальная скорость", + "maximumVelocity": "Максимальная скорость", + "maximumDuration": "Максимальная продолжительность", + "maximumTravel": "Максимальный ход", + "decayTime": "Время распада", + "pixelsPerTick": "Пикселей на такт", + "maximumFrameInterval": "Максимальный интервал кадров", + "maximumTicksPerFrame": "Максимальное количество тактов на кадр" + }, + "imageStorage": { + "instanceId": "Идентификатор экземпляра терминальной сессии", + "instanceIdDesc": "Идентификатор экземпляра одной из подключенных вкладок терминала, используемый для проверки видимости.", + "selectedModeUnavailable": "Недоступно", + "test": "Тест", + "testing": "Тестирование...", + "testFailed": "Не удалось проверить хранилище изображений.", + "testConnected": "Терминальная сессия подключена.", + "testNotConnected": "Нет подключенной терминальной сессии с этим идентификатором экземпляра.", + "testSelectedMode": "Для загрузки будет использоваться следующий синтаксис: {{mode}}", + "testLocalVisible": "Локальный каталог виден с подключенного хоста.", + "testLocalNotVisible": "Локальный каталог НЕ виден с подключенного хоста." + }, + "settings": { + "admin": { + "group": { + "touch": "Сенсорный ввод", + "images": "Хранилище изображений" + }, + "sessionTimeoutMinutes": { + "label": "Сохранение сессии в терминале", + "description": "Как долго отключенный терминал остается активным на сервере до его закрытия. Мин. 1 · Макс. 1440 минут. Для сессий, которые должны длиться дольше этого времени, включите Auto-Tmux на хосте." + }, + "commandHistoryEnabled": { + "label": "История команд", + "description": "Включить запись истории команд. При отключении история не сохраняется независимо от настроек хостов." + }, + "imageStorageMode": { + "label": "Режим хранения", + "description": "Где записываются и загружаются образы терминала. Автоматический выбор по типу подключения, локальному и удаленному (SFTP) — это фиксированная настройка.", + "auto": "Авто", + "local": "Местный", + "remoteSftp": "Удаленный (SFTP)" + }, + "imageLocalDir": { + "label": "Локальный каталог" + }, + "imageHostPath": { + "label": "Путь к хосту", + "description": "Представление локального каталога на стороне хоста, которое получают терминальные агенты." + }, + "imageTtlMs": { + "label": "TTL (мс)" + }, + "imageMaxCount": { + "label": "Максимальное количество изображений" + }, + "imageMaxBytes": { + "label": "Максимальный объем хранилища в байтах" + } + }, + "host": { + "enableTerminalToolbar": { + "label": "Включить панель инструментов терминала", + "description": "Отобразить панель быстрого доступа, закрепленную на активном терминале, для отображения инструментов и статистики этого хоста в режиме реального времени." + }, + "enableCommandHistory": { + "label": "История команд", + "description": "Сохранять команды, выполняемые в этом терминале, для истории и автодополнения" + }, + "enableTerminal": { + "label": "Включить терминал" + } + } + }, + "palette": { + "localTerminal": "Локальный терминал" + } +} diff --git a/plugins/ssh-terminal/locales/translated/sr_SP.json b/plugins/ssh-terminal/locales/translated/sr_SP.json new file mode 100644 index 000000000..937a89efd --- /dev/null +++ b/plugins/ssh-terminal/locales/translated/sr_SP.json @@ -0,0 +1,203 @@ +{ + "mobileKeyboard": { + "addQuickKey": "Додај", + "arrowDown": "Стрелица надоле", + "arrowLeft": "Стрелица лево", + "arrowRight": "Стрелица десно", + "arrowUp": "Стрелица нагоре", + "backTab": "⇥", + "ctrl": "Ctrl", + "delete": "Дел", + "done": "Готово", + "editQuickKeys": "Уреди брзе тастере", + "end": "Крај", + "esc": "Излаз", + "home": "Дом", + "pageDown": "ПгДн", + "pageUp": "PgUp", + "paste": "Залепи", + "quickKeyPlaceholder": "нпр. sudo ", + "quickKeysDesc": "Додирните × да бисте уклонили. Подржава до 8 знакова.", + "quickKeysTitle": "Брзи тастери", + "removeQuickKey": "Уклони", + "resetDefaults": "Ресетуј на подразумеване вредности", + "shift": "Померање", + "tab": "Таб" + }, + "terminal": { + "autoTmuxEnableFailed": "Није успело омогућавање аутоматског Tmux-а", + "autoTmuxEnabled": "Аутоматски Tmux је омогућен за {{host}}. Примењује се од следећег повезивања.", + "clipboardHttpWarning": "Није могуће прочитати међуспремник. Неки прегледачи ограничавају приступ међуспремнику ван HTTPS-а - покушајте са Ctrl+V или покрените Termix преко HTTPS-а.", + "closeTab": "Затвори", + "connecting": "Повезивање...", + "connectionLost": "Веза је прекинута", + "connectionRejected": "Сервер је одбио везу. Проверите аутентификацију и конфигурацију мреже.", + "connectionTimeout": "Временско ограничење везе", + "enableAutoTmuxAction": "Омогући аутоматски Tmux", + "hostKeyRejected": "Верификација SSH кључа хоста је одбијена. Веза је отказана.", + "linkDialogCopy": "Копија", + "linkDialogOpen": "Отворено", + "linkDialogTitle": "Отвори линк", + "maxReconnectAttemptsReached": "Достигнут је максималан број покушаја поновног повезивања", + "noHostSelected": "Није изабран домаћин", + "opksshAuthFailed": "Аутентификација није успела. Проверите своје акредитиве и покушајте поново.", + "opksshTimeout": "Временско ограничење за аутентификацију је истекло. Молимо покушајте поново.", + "passwordPromptFillTitle": "Унесите сачувану лозинку у овај промпт?", + "reconnect": "Поново се повежите", + "reconnected": "Поново повезано успешно", + "reconnecting": "Поновно повезивање... ({{attempt}}/{{max}})", + "searchCaseSensitive": "Упаривање великог и малог дела", + "searchClose": "Затвори (Escape)", + "searchNext": "Следећи меч (Унесите)", + "searchNoResults": "Нема резултата", + "searchPlaceholder": "Пронађи", + "searchPrevious": "Претходно подударање (Shift+Enter)", + "searchRegex": "Користите регуларни израз", + "searchResultCount": "{{index}} од {{count}}", + "searchWholeWord": "Подударање целе речи", + "sessionExpiredNotice": "Ваша претходна сесија на {{host}} је истекла након {{minutes}} минута од прекида везе; љуска и све што је у њој покренуто су нестали. Омогућите Auto-Tmux на овом хосту да би сесије остале активне.", + "sessionExpiredNoticeNoMinutes": "Ваша претходна сесија на {{host}} је истекла док сте били одвојени; љуска и све што је у њој покренуто су нестали. Омогућите Auto-Tmux на овом хосту да би сесије остале активне.", + "sessionTakenOver": "Сесија је отворена у другој картици. Поновно повезивање...", + "tmuxAttached": "Прикључени клијенти", + "tmuxAttachedCount": "{{count}} приложено", + "tmuxCopyHint": "Прилагодите избор и притисните Ентер да бисте копирали у међуспремник", + "tmuxCreateNew": "Започни нову сесију", + "tmuxDetached": "Одвојено од tmux сесије", + "tmuxLastActivity": "Последња активност", + "tmuxSessionAttached": "приложена tmux сесија: {{name}}", + "tmuxSessionCreated": "tmux сесија креирана: {{name}}", + "tmuxSessionPickerDesc": "Постојеће tmux сесије пронађене су на овом хосту. Изаберите једну да бисте је поново повезали или креирали нову сесију.", + "tmuxSessionPickerTitle": "tmux сесије", + "tmuxTimeDays": "{{count}}пре д", + "tmuxTimeHours": "{{count}}пре сати", + "tmuxTimeJustNow": "управо сада", + "tmuxTimeMinutes": "{{count}}пре мин", + "tmuxUnavailable": "tmux није инсталиран на удаљеном хосту, враћа се на стандардну љуску", + "tmuxWindowCount": "{{count}} прозор", + "tmuxWindows": "Прозори", + "unknownError": "Дошло је до непознате грешке", + "vaultAuthDescription": "Отворио се прозор за пријављивање на HashiCorp Vault. Завршите пријављивање тамо; ова веза ће се аутоматски наставити.", + "vaultAuthFailed": "Аутентификација трезора није успела. Молимо покушајте поново.", + "vaultAuthTitle": "Потребно је пријављивање у трезор", + "vaultReopen": "Поново отвори прозор за пријављивање", + "websocketAbnormalClose": "Веза је неочекивано прекинута. Ово може бити због проблема са обрнутим проксијем или SSL конфигурацијом. Молимо вас да проверите логове сервера.", + "websocketError": "Грешка у повезивању са WebSocket-ом" + }, + "terminalToolbar": { + "detachTmux": "Одвојите tmux", + "detachTmuxDescription": "Одвојите tmux сесију", + "dismissError": "Грешка при одбацивању слике", + "hideToolbar": "Сакриј траку са алаткама", + "image": "Радње са сликама (само слике)", + "imageActionFailed": "Акција са сликом није успела", + "layout": "Режим приказа алатне траке", + "layoutExpanded": "Проширено са метрикама", + "layoutIcon": "Само икона", + "layoutLabeled": "Иконе и ознаке", + "moveToolbar": "Помери траку са алаткама.", + "moveToolbarHint": "Превуците да бисте променили положај траке са алаткама", + "paste": "Налепи слику", + "pasteImage": "Налепи слику из међуспремника (само слике)", + "retry": "Покушај поново", + "showToolbar": "Прошири траку са алаткама", + "upload": "Отпреми слику", + "uploadImage": "Отпремите слику на хост", + "uploadingImage": "Отпремање..." + }, + "history": { + "noTerminalSelected": "Није изабран терминал", + "noTerminalSelectedHint": "Отворите картицу SSH терминала да бисте видели историју његових команди", + "searchPlaceholder": "Историја претраге...", + "clearAll": "Обриши све", + "noHistoryEntries": "Нема уноса у историји", + "trackingDisabled": "Праћење историје је онемогућено" + }, + "touchInput": { + "enabled": "Омогући унос додиром", + "enabledDesc": "Дозволите скроловање једним прстом у терминалима", + "momentumEnabled": "Омогући Моментум", + "momentumEnabledDesc": "Наставите кратко да скролујете након превлачења прстом", + "advanced": "Напредно", + "resetDefaults": "Ресетуј на подразумеване вредности", + "dragThreshold": "Праг превлачења", + "maxWheelDelta": "Максимална делта точка", + "momentumSampleWindow": "Прозор узорка момента", + "releaseGracePeriod": "Грејс период за објављивање", + "minimumVelocity": "Минимална брзина", + "maximumVelocity": "Максимална брзина", + "maximumDuration": "Максимално трајање", + "maximumTravel": "Максимални ход", + "decayTime": "Време распада", + "pixelsPerTick": "Пиксела по тик-у", + "maximumFrameInterval": "Максимални интервал кадра", + "maximumTicksPerFrame": "Максималан број откуцаја по кадру" + }, + "imageStorage": { + "instanceId": "ИД инстанце терминалне сесије", + "instanceIdDesc": "ИД инстанце једне од ваших повезаних картица терминала, која се користи за тестирање видљивости.", + "selectedModeUnavailable": "Недоступно", + "test": "Тест", + "testing": "Тестирање...", + "testFailed": "Тестирање складиштења слика није успело", + "testConnected": "Терминална сесија је повезана.", + "testNotConnected": "Нема повезане терминалне сесије са овим ID-ом инстанце.", + "testSelectedMode": "Отпремање би користило: {{mode}}", + "testLocalVisible": "Локални директоријум је видљив са повезаног хоста.", + "testLocalNotVisible": "Локални директоријум НИЈЕ видљив са повезаног хоста." + }, + "settings": { + "admin": { + "group": { + "touch": "Унос додиром", + "images": "Складиштење слика" + }, + "sessionTimeoutMinutes": { + "label": "Трајност терминалне сесије", + "description": "Колико дуго се искључени терминал одржава активним на серверу пре него што се затвори. Мин. 1 · Макс. 1440 минута. За сесије које морају да трају дуже од овог периода, омогућите Auto-Tmux на хосту." + }, + "commandHistoryEnabled": { + "label": "Историја команди", + "description": "Дозволи снимање историје команди. Када је онемогућено, историја се не чува без обзира на подешавања за сваки хост." + }, + "imageStorageMode": { + "label": "Режим складиштења", + "description": "Где се записују налепљене и отпремљене слике терминала. Аутоматски избори по конекцији, локални и удаљени (SFTP) су поправљени.", + "auto": "Аутоматски", + "local": "Локално", + "remoteSftp": "Удаљено (SFTP)" + }, + "imageLocalDir": { + "label": "Локални директоријум" + }, + "imageHostPath": { + "label": "Путања хоста", + "description": "Поглед са стране хоста на локални директоријум који терминални агенти примају." + }, + "imageTtlMs": { + "label": "TTL (ms)" + }, + "imageMaxCount": { + "label": "Максималан број слика" + }, + "imageMaxBytes": { + "label": "Максимални број бајтова за складиштење" + } + }, + "host": { + "enableTerminalToolbar": { + "label": "Омогући траку са алаткама терминала", + "description": "Прикажи траку са алаткама за брзи приступ причвршћену за активни терминал за алате и статистику уживо овог хоста" + }, + "enableCommandHistory": { + "label": "Историја команди", + "description": "Снимање команди које се извршавају у овом терминалу за историју и аутоматско довршавање" + }, + "enableTerminal": { + "label": "Омогући терминал" + } + } + }, + "palette": { + "localTerminal": "Локални терминал" + } +} diff --git a/plugins/ssh-terminal/locales/translated/sv_SE.json b/plugins/ssh-terminal/locales/translated/sv_SE.json new file mode 100644 index 000000000..ebcb6aed2 --- /dev/null +++ b/plugins/ssh-terminal/locales/translated/sv_SE.json @@ -0,0 +1,203 @@ +{ + "mobileKeyboard": { + "addQuickKey": "Tillägga", + "arrowDown": "Pil nedåt", + "arrowLeft": "Pil vänster", + "arrowRight": "Pil höger", + "arrowUp": "Pil upp", + "backTab": "⇥", + "ctrl": "Ctrl", + "delete": "Del", + "done": "Gjort", + "editQuickKeys": "Redigera snabbknappar", + "end": "Avsluta", + "esc": "Esc", + "home": "Hem", + "pageDown": "PgDn", + "pageUp": "PgUp", + "paste": "Klistra", + "quickKeyPlaceholder": "t.ex. sudo ", + "quickKeysDesc": "Tryck på × för att ta bort. Stöder upp till 8 tecken.", + "quickKeysTitle": "Snabbtangenter", + "removeQuickKey": "Ta bort", + "resetDefaults": "Återställ till standardinställningarna", + "shift": "Flytta", + "tab": "Flik" + }, + "terminal": { + "autoTmuxEnableFailed": "Misslyckades med att aktivera Auto-Tmux", + "autoTmuxEnabled": "Auto-Tmux aktiverat för {{host}}. Det gäller från nästa anslutning.", + "clipboardHttpWarning": "Kunde inte läsa urklippet. Vissa webbläsare begränsar åtkomst till urklipp utanför HTTPS - prova Ctrl+V istället, eller servera Termix över HTTPS.", + "closeTab": "Nära", + "connecting": "Ansluter...", + "connectionLost": "Anslutningen förlorades", + "connectionRejected": "Anslutningen avvisades av servern. Kontrollera din autentisering och nätverkskonfiguration.", + "connectionTimeout": "Anslutningstimeout", + "enableAutoTmuxAction": "Aktivera Auto-Tmux", + "hostKeyRejected": "Verifiering av SSH-värdnyckel avvisad. Anslutningen avbröts.", + "linkDialogCopy": "Kopiera", + "linkDialogOpen": "Öppna", + "linkDialogTitle": "Öppna länk", + "maxReconnectAttemptsReached": "Maximalt antal återanslutningsförsök uppnådda", + "noHostSelected": "Ingen värd vald", + "opksshAuthFailed": "Autentiseringen misslyckades. Kontrollera dina inloggningsuppgifter och försök igen.", + "opksshTimeout": "Autentiseringen har gått ut. Försök igen.", + "passwordPromptFillTitle": "Fyll i det sparade lösenordet i den här frågan?", + "reconnect": "Återanslut", + "reconnected": "Återansluten lyckades", + "reconnecting": "Återansluter... ({{attempt}}/{{max}})", + "searchCaseSensitive": "Matchfodral", + "searchClose": "Stäng (Escape)", + "searchNext": "Nästa match (Enter)", + "searchNoResults": "Inga resultat", + "searchPlaceholder": "Hitta", + "searchPrevious": "Föregående matchning (Shift+Enter)", + "searchRegex": "Använd reguljärt uttryck", + "searchResultCount": "{{index}} av {{count}}", + "searchWholeWord": "Matcha hela ordet", + "sessionExpiredNotice": "Din föregående session på {{host}} gick ut efter {{minutes}} minuters frånkoppling; skalet och allt som körs i det är borta. Aktivera Auto-Tmux på den här värden för att hålla sessionerna aktiva.", + "sessionExpiredNoticeNoMinutes": "Din föregående session på {{host}} upphörde medan den var frånkopplad; skalet och allt som körs i det är borta. Aktivera Auto-Tmux på den här värden för att hålla sessionerna aktiva.", + "sessionTakenOver": "Sessionen öppnades i en annan flik. Återansluter...", + "tmuxAttached": "Anslutna klienter", + "tmuxAttachedCount": "{{count}} bifogad", + "tmuxCopyHint": "Justera markeringen och tryck på Enter för att kopiera till urklipp", + "tmuxCreateNew": "Starta ny session", + "tmuxDetached": "Losskopplad från tmux-sessionen", + "tmuxLastActivity": "Senaste aktivitet", + "tmuxSessionAttached": "tmux-session bifogad: {{name}}", + "tmuxSessionCreated": "tmux-session skapad: {{name}}", + "tmuxSessionPickerDesc": "Befintliga tmux-sessioner hittades på den här värden. Välj en för att återansluta eller skapa en ny session.", + "tmuxSessionPickerTitle": "tmux-sessioner", + "tmuxTimeDays": "för {{count}}dagar sedan", + "tmuxTimeHours": "för {{count}}timmar sedan", + "tmuxTimeJustNow": "just nu", + "tmuxTimeMinutes": "för {{count}}minuter sedan", + "tmuxUnavailable": "tmux är inte installerat på fjärrdatorn, utan återgår till standardskal", + "tmuxWindowCount": "{{count}} fönster", + "tmuxWindows": "Fönster", + "unknownError": "Okänt fel inträffade", + "vaultAuthDescription": "Ett fönster har öppnats för att logga in på HashiCorp Vault. Slutför inloggningen där; anslutningen fortsätter automatiskt.", + "vaultAuthFailed": "Valvautentisering misslyckades. Försök igen.", + "vaultAuthTitle": "Inloggning till arkivet krävs", + "vaultReopen": "Öppna inloggningsfönstret igen", + "websocketAbnormalClose": "Anslutningen stängdes oväntat. Detta kan bero på ett problem med omvänd proxy eller SSL-konfiguration. Kontrollera serverloggarna.", + "websocketError": "WebSocket-anslutningsfel" + }, + "terminalToolbar": { + "detachTmux": "Lossa tmux", + "detachTmuxDescription": "Koppla bort tmux-session", + "dismissError": "Stäng bildfel", + "hideToolbar": "Dölj verktygsfältet", + "image": "Bildåtgärder (endast bilder)", + "imageActionFailed": "Bildåtgärden misslyckades", + "layout": "Verktygsfältets visningsläge", + "layoutExpanded": "Utökad med mätvärden", + "layoutIcon": "Endast ikon", + "layoutLabeled": "Ikoner och etiketter", + "moveToolbar": "Flytta verktygsfältet", + "moveToolbarHint": "Dra för att flytta verktygsfältet", + "paste": "Klistra in bild", + "pasteImage": "Klistra in bild från urklipp (endast bilder)", + "retry": "Försöka igen", + "showToolbar": "Expandera verktygsfältet", + "upload": "Ladda upp bild", + "uploadImage": "Ladda upp en bild till värden", + "uploadingImage": "Laddar upp..." + }, + "history": { + "noTerminalSelected": "Ingen terminal vald", + "noTerminalSelectedHint": "Öppna en SSH-terminalflik för att visa dess kommandohistorik", + "searchPlaceholder": "Sökhistorik...", + "clearAll": "Rensa alla", + "noHistoryEntries": "Inga historikposter", + "trackingDisabled": "Historikspårning är inaktiverad" + }, + "touchInput": { + "enabled": "Aktivera pekinmatning", + "enabledDesc": "Tillåt scrollning med ett finger i terminaler", + "momentumEnabled": "Aktivera momentum", + "momentumEnabledDesc": "Fortsätt skrolla kort efter en svepning", + "advanced": "Avancerad", + "resetDefaults": "Återställ till standardinställningarna", + "dragThreshold": "Draggräns", + "maxWheelDelta": "Maximal hjuldelta", + "momentumSampleWindow": "Momentum-exempelfönster", + "releaseGracePeriod": "Frigivningsfrist", + "minimumVelocity": "Minsta hastighet", + "maximumVelocity": "Maximal hastighet", + "maximumDuration": "Maximal varaktighet", + "maximumTravel": "Maximal resa", + "decayTime": "Avklingningstid", + "pixelsPerTick": "Pixlar per tick", + "maximumFrameInterval": "Maximalt bildintervall", + "maximumTicksPerFrame": "Maximalt antal ticks per bildruta" + }, + "imageStorage": { + "instanceId": "Terminalsessionsinstans-ID", + "instanceIdDesc": "Instans-ID för en av dina anslutna terminalflikar, används för att testa synligheten.", + "selectedModeUnavailable": "Inte tillgänglig", + "test": "Testa", + "testing": "Testning...", + "testFailed": "Misslyckades med att testa bildlagring", + "testConnected": "Terminalsessionen är ansluten.", + "testNotConnected": "Ingen ansluten terminalsession med detta instans-ID.", + "testSelectedMode": "En uppladdning skulle använda: {{mode}}", + "testLocalVisible": "Den lokala katalogen är synlig från den anslutna värden.", + "testLocalNotVisible": "Den lokala katalogen är INTE synlig från den anslutna värden." + }, + "settings": { + "admin": { + "group": { + "touch": "Pekinmatning", + "images": "Bildlagring" + }, + "sessionTimeoutMinutes": { + "label": "Terminalsessionens persistens", + "description": "Hur länge en frånkopplad terminal hålls aktiv på servern innan den stängs. Min 1 · Max 1440 minuter. För sessioner som måste överleva detta, aktivera Auto-Tmux på värden." + }, + "commandHistoryEnabled": { + "label": "Kommandohistorik", + "description": "Tillåt inspelning av kommandohistorik. När det är inaktiverat sparas inte historiken oavsett inställningarna per värd." + }, + "imageStorageMode": { + "label": "Lagringsläge", + "description": "Där inklistrade och uppladdade terminalbilder skrivs. Automatiska val via anslutning, lokal och fjärransluten (SFTP) är åtgärdade.", + "auto": "Bil", + "local": "Lokal", + "remoteSftp": "Fjärrstyrning (SFTP)" + }, + "imageLocalDir": { + "label": "Lokal katalog" + }, + "imageHostPath": { + "label": "Värdsökväg", + "description": "Värdsidans vy av den lokala katalogen som terminalagenter tar emot." + }, + "imageTtlMs": { + "label": "TTL (ms)" + }, + "imageMaxCount": { + "label": "Max antal bilder" + }, + "imageMaxBytes": { + "label": "Max antal lagringsbyte" + } + }, + "host": { + "enableTerminalToolbar": { + "label": "Aktivera terminalverktygsfältet", + "description": "Visa en snabbåtkomstverktygsfält dockad till den aktiva terminalen för den här värdens verktyg och livestatistik" + }, + "enableCommandHistory": { + "label": "Kommandohistorik", + "description": "Record-kommandon körs i den här terminalen för historik och autofullständig ifyllning" + }, + "enableTerminal": { + "label": "Aktivera terminalen" + } + } + }, + "palette": { + "localTerminal": "Lokal terminal" + } +} diff --git a/plugins/ssh-terminal/locales/translated/th_TH.json b/plugins/ssh-terminal/locales/translated/th_TH.json new file mode 100644 index 000000000..9fbb74767 --- /dev/null +++ b/plugins/ssh-terminal/locales/translated/th_TH.json @@ -0,0 +1,203 @@ +{ + "mobileKeyboard": { + "addQuickKey": "เพิ่ม", + "arrowDown": "ลูกศรลง", + "arrowLeft": "ลูกศรซ้าย", + "arrowRight": "ลูกศรขวา", + "arrowUp": "ลูกศรขึ้น", + "backTab": "⇥", + "ctrl": "Ctrl", + "delete": "เดล", + "done": "เสร็จแล้ว", + "editQuickKeys": "แก้ไขปุ่มลัด", + "end": "จบ", + "esc": "เอส", + "home": "บ้าน", + "pageDown": "พีจีดีเอ็น", + "pageUp": "พีจีอัพ", + "paste": "แปะ", + "quickKeyPlaceholder": "เช่น sudo ", + "quickKeysDesc": "แตะ × เพื่อลบ รองรับได้สูงสุด 8 ตัวอักษร", + "quickKeysTitle": "ปุ่มลัด", + "removeQuickKey": "ลบ", + "resetDefaults": "รีเซ็ตเป็นการตั้งค่าเริ่มต้น", + "shift": "กะ", + "tab": "แท็บ" + }, + "terminal": { + "autoTmuxEnableFailed": "ไม่สามารถเปิดใช้งาน Auto-Tmux ได้", + "autoTmuxEnabled": "เปิดใช้งาน Auto-Tmux สำหรับ {{host}}แล้ว จะมีผลตั้งแต่การเชื่อมต่อครั้งถัดไป", + "clipboardHttpWarning": "ไม่สามารถอ่านข้อมูลในคลิปบอร์ดได้ เบราว์เซอร์บางตัวจำกัดการเข้าถึงคลิปบอร์ดนอก HTTPS - ลองใช้ Ctrl+V แทน หรือใช้งาน Termix ผ่าน HTTPS", + "closeTab": "ปิด", + "connecting": "กำลังเชื่อมต่อ...", + "connectionLost": "การเชื่อมต่อขาดหาย", + "connectionRejected": "เซิร์ฟเวอร์ปฏิเสธการเชื่อมต่อ โปรดตรวจสอบการยืนยันตัวตนและการกำหนดค่าเครือข่ายของคุณ", + "connectionTimeout": "หมดเวลาการเชื่อมต่อ", + "enableAutoTmuxAction": "เปิดใช้งาน Auto-Tmux", + "hostKeyRejected": "การตรวจสอบคีย์โฮสต์ SSH ถูกปฏิเสธ การเชื่อมต่อถูกยกเลิก", + "linkDialogCopy": "สำเนา", + "linkDialogOpen": "เปิด", + "linkDialogTitle": "เปิดลิงก์", + "maxReconnectAttemptsReached": "จำนวนครั้งการเชื่อมต่อใหม่สูงสุดครบแล้ว", + "noHostSelected": "ไม่ได้เลือกโฮสต์", + "opksshAuthFailed": "การตรวจสอบสิทธิ์ล้มเหลว โปรดตรวจสอบข้อมูลประจำตัวของคุณและลองอีกครั้ง", + "opksshTimeout": "การตรวจสอบสิทธิ์หมดเวลา โปรดลองอีกครั้ง", + "passwordPromptFillTitle": "กรอกรหัสผ่านที่บันทึกไว้ลงในช่องนี้ใช่หรือไม่?", + "reconnect": "เชื่อมต่อใหม่", + "reconnected": "เชื่อมต่อสำเร็จแล้ว", + "reconnecting": "กำลังเชื่อมต่อใหม่... ({{attempt}}/{{max}})", + "searchCaseSensitive": "กล่องไม้ขีดไฟ", + "searchClose": "ปิด (หนี)", + "searchNext": "การแข่งขันถัดไป (กดเข้าร่วม)", + "searchNoResults": "ไม่พบผลลัพธ์", + "searchPlaceholder": "หา", + "searchPrevious": "การแข่งขันครั้งก่อนหน้า (Shift+Enter)", + "searchRegex": "ใช้ Regular Expression", + "searchResultCount": "{{index}} ของ {{count}}", + "searchWholeWord": "จับคู่คำทั้งหมด", + "sessionExpiredNotice": "เซสชันก่อนหน้าของคุณบน {{host}} หมดอายุหลังจาก {{minutes}} นาทีถูกตัดการเชื่อมต่อ เชลล์และทุกสิ่งที่กำลังทำงานอยู่ภายในนั้นหายไปแล้ว โปรดเปิดใช้งาน Auto-Tmux บนโฮสต์นี้เพื่อรักษาสถานะเซสชันให้คงอยู่", + "sessionExpiredNoticeNoMinutes": "เซสชันก่อนหน้าของคุณบน {{host}} หมดอายุขณะที่ไม่ได้เชื่อมต่อ เชลล์และทุกอย่างที่กำลังทำงานอยู่ภายในนั้นหายไปแล้ว โปรดเปิดใช้งาน Auto-Tmux บนโฮสต์นี้เพื่อรักษาสถานะเซสชันให้คงอยู่", + "sessionTakenOver": "เซสชั่นนี้เปิดอยู่ในแท็บอื่น กำลังเชื่อมต่อใหม่...", + "tmuxAttached": "ลูกค้าที่แนบมาด้วย", + "tmuxAttachedCount": "{{count}} แนบมาด้วย", + "tmuxCopyHint": "ปรับตำแหน่งที่เลือกแล้วกด Enter เพื่อคัดลอกไปยังคลิปบอร์ด", + "tmuxCreateNew": "เริ่มเซสชั่นใหม่", + "tmuxDetached": "ตัดการเชื่อมต่อจากเซสชัน tmux แล้ว", + "tmuxLastActivity": "กิจกรรมล่าสุด", + "tmuxSessionAttached": "การเชื่อมต่อเซสชัน tmux: {{name}}", + "tmuxSessionCreated": "สร้างเซสชัน tmux แล้ว: {{name}}", + "tmuxSessionPickerDesc": "พบเซสชัน tmux ที่มีอยู่แล้วในโฮสต์นี้ เลือกหนึ่งเซสชันเพื่อเชื่อมต่อใหม่หรือสร้างเซสชันใหม่", + "tmuxSessionPickerTitle": "เซสชัน tmux", + "tmuxTimeDays": "{{count}}วันก่อน", + "tmuxTimeHours": "{{count}}ชั่วโมงที่แล้ว", + "tmuxTimeJustNow": "เมื่อสักครู่นี้เอง", + "tmuxTimeMinutes": "{{count}}นาทีที่แล้ว", + "tmuxUnavailable": "tmux ไม่ได้ติดตั้งอยู่บนโฮสต์ระยะไกล จึงใช้เชลล์มาตรฐานแทน", + "tmuxWindowCount": "{{count}} หน้าต่าง", + "tmuxWindows": "วินโดวส์", + "unknownError": "เกิดข้อผิดพลาดที่ไม่ทราบสาเหตุ", + "vaultAuthDescription": "หน้าต่างสำหรับเข้าสู่ระบบ HashiCorp Vault ได้เปิดขึ้นแล้ว โปรดทำการเข้าสู่ระบบให้เสร็จสมบูรณ์ การเชื่อมต่อนี้จะดำเนินการต่อโดยอัตโนมัติ", + "vaultAuthFailed": "การตรวจสอบสิทธิ์ Vault ล้มเหลว โปรดลองอีกครั้ง", + "vaultAuthTitle": "ต้องลงชื่อเข้าใช้ Vault", + "vaultReopen": "เปิดหน้าต่างเข้าสู่ระบบอีกครั้ง", + "websocketAbnormalClose": "การเชื่อมต่อถูกตัดโดยไม่คาดคิด อาจเกิดจากปัญหาเกี่ยวกับรีเวิร์สพร็อกซีหรือการกำหนดค่า SSL โปรดตรวจสอบบันทึกของเซิร์ฟเวอร์", + "websocketError": "ข้อผิดพลาดในการเชื่อมต่อ WebSocket" + }, + "terminalToolbar": { + "detachTmux": "ถอด tmux ออก", + "detachTmuxDescription": "ยกเลิกการเชื่อมต่อเซสชัน tmux", + "dismissError": "แก้ไขข้อผิดพลาดเกี่ยวกับรูปภาพ", + "hideToolbar": "ซ่อนแถบเครื่องมือ", + "image": "การดำเนินการกับรูปภาพ (เฉพาะรูปภาพ)", + "imageActionFailed": "การดำเนินการรูปภาพล้มเหลว", + "layout": "โหมดแสดงแถบเครื่องมือ", + "layoutExpanded": "ขยายเพิ่มเติมด้วยตัวชี้วัด", + "layoutIcon": "เฉพาะไอคอน", + "layoutLabeled": "ไอคอนและป้ายกำกับ", + "moveToolbar": "ย้ายแถบเครื่องมือ", + "moveToolbarHint": "ลากเพื่อจัดตำแหน่งแถบเครื่องมือใหม่", + "paste": "วางรูปภาพ", + "pasteImage": "วางภาพจากคลิปบอร์ด (เฉพาะภาพ)", + "retry": "ลองใหม่อีกครั้ง", + "showToolbar": "ขยายแถบเครื่องมือ", + "upload": "อัปโหลดรูปภาพ", + "uploadImage": "อัปโหลดรูปภาพไปยังโฮสต์", + "uploadingImage": "กำลังอัปโหลด..." + }, + "history": { + "noTerminalSelected": "ไม่ได้เลือกเทอร์มินัล", + "noTerminalSelectedHint": "เปิดแท็บเทอร์มินัล SSH เพื่อดูประวัติคำสั่ง", + "searchPlaceholder": "ประวัติการค้นหา...", + "clearAll": "ล้างทั้งหมด", + "noHistoryEntries": "ไม่มีรายการประวัติ", + "trackingDisabled": "ปิดใช้งานการติดตามประวัติแล้ว" + }, + "touchInput": { + "enabled": "เปิดใช้งานการป้อนข้อมูลแบบสัมผัส", + "enabledDesc": "อนุญาตให้เลื่อนหน้าจอด้วยนิ้วเดียวในเทอร์มินัล", + "momentumEnabled": "เปิดใช้งานโมเมนตัม", + "momentumEnabledDesc": "เลื่อนหน้าจอต่ออีกสักครู่หลังจากปัดนิ้ว", + "advanced": "ขั้นสูง", + "resetDefaults": "รีเซ็ตเป็นการตั้งค่าเริ่มต้น", + "dragThreshold": "เกณฑ์การลาก", + "maxWheelDelta": "เดลต้าล้อสูงสุด", + "momentumSampleWindow": "หน้าต่างตัวอย่างโมเมนตัม", + "releaseGracePeriod": "ระยะเวลาผ่อนผันการปล่อยตัว", + "minimumVelocity": "ความเร็วต่ำสุด", + "maximumVelocity": "ความเร็วสูงสุด", + "maximumDuration": "ระยะเวลาสูงสุด", + "maximumTravel": "การเดินทางสูงสุด", + "decayTime": "เวลาการเสื่อมสภาพ", + "pixelsPerTick": "พิกเซลต่อติ๊ก", + "maximumFrameInterval": "ช่วงเวลาเฟรมสูงสุด", + "maximumTicksPerFrame": "จำนวนขีดสูงสุดต่อเฟรม" + }, + "imageStorage": { + "instanceId": "รหัสอินสแตนซ์เซสชันเทอร์มินัล", + "instanceIdDesc": "รหัสอินสแตนซ์ของแท็บเทอร์มินัลที่เชื่อมต่ออยู่แท็บหนึ่ง ใช้สำหรับทดสอบการมองเห็น", + "selectedModeUnavailable": "ไม่พร้อมใช้งาน", + "test": "ทดสอบ", + "testing": "กำลังทดสอบ...", + "testFailed": "การทดสอบการจัดเก็บรูปภาพล้มเหลว", + "testConnected": "การเชื่อมต่อเซสชันเทอร์มินัลเสร็จสมบูรณ์แล้ว", + "testNotConnected": "ไม่มีเซสชันเทอร์มินัลที่เชื่อมต่อกับรหัสอินสแตนซ์นี้", + "testSelectedMode": "การอัปโหลดจะใช้รูปแบบ: {{mode}}", + "testLocalVisible": "ไดเร็กทอรีภายในเครื่องสามารถมองเห็นได้จากโฮสต์ที่เชื่อมต่ออยู่", + "testLocalNotVisible": "ไดเร็กทอรีภายในเครื่องจะไม่สามารถมองเห็นได้จากโฮสต์ที่เชื่อมต่ออยู่" + }, + "settings": { + "admin": { + "group": { + "touch": "การป้อนข้อมูลด้วยระบบสัมผัส", + "images": "การจัดเก็บรูปภาพ" + }, + "sessionTimeoutMinutes": { + "label": "การคงอยู่ของเซสชันเทอร์มินัล", + "description": "ระยะเวลาที่เทอร์มินัลที่ไม่ได้เชื่อมต่อจะยังคงทำงานอยู่บนเซิร์ฟเวอร์ก่อนที่จะถูกปิด ต่ำสุด 1 นาที สูงสุด 1440 นาที สำหรับเซสชันที่ต้องใช้งานนานกว่านี้ ให้เปิดใช้งาน Auto-Tmux บนโฮสต์" + }, + "commandHistoryEnabled": { + "label": "ประวัติคำสั่ง", + "description": "อนุญาตให้บันทึกประวัติคำสั่ง เมื่อปิดใช้งาน ประวัติจะไม่ถูกบันทึกไม่ว่าการตั้งค่าต่อโฮสต์จะเป็นอย่างไรก็ตาม" + }, + "imageStorageMode": { + "label": "โหมดจัดเก็บข้อมูล", + "description": "ตำแหน่งที่จะบันทึกภาพเทอร์มินัลที่คัดลอกและอัปโหลด การเลือกอัตโนมัติตามการเชื่อมต่อ ทั้งแบบโลคอลและรีโมต (SFTP) จะถูกกำหนดไว้ตายตัว", + "auto": "อัตโนมัติ", + "local": "ท้องถิ่น", + "remoteSftp": "รีโมท (SFTP)" + }, + "imageLocalDir": { + "label": "ไดเร็กทอรีท้องถิ่น" + }, + "imageHostPath": { + "label": "เส้นทางโฮสต์", + "description": "มุมมองฝั่งโฮสต์ของไดเร็กทอรีท้องถิ่นที่เอเจนต์เทอร์มินัลได้รับ" + }, + "imageTtlMs": { + "label": "ทีทีแอล (มิลลิวินาที)" + }, + "imageMaxCount": { + "label": "จำนวนภาพสูงสุด" + }, + "imageMaxBytes": { + "label": "พื้นที่จัดเก็บสูงสุด (ไบต์)" + } + }, + "host": { + "enableTerminalToolbar": { + "label": "เปิดใช้งานแถบเครื่องมือเทอร์มินัล", + "description": "แสดงแถบเครื่องมือเข้าถึงด่วนที่ตรึงอยู่กับเทอร์มินัลที่ใช้งานอยู่ สำหรับเครื่องมือและสถิติแบบเรียลไทม์ของโฮสต์นี้" + }, + "enableCommandHistory": { + "label": "ประวัติคำสั่ง", + "description": "บันทึกคำสั่งที่เรียกใช้ในเทอร์มินัลนี้เพื่อดูประวัติและฟังก์ชันเติมคำอัตโนมัติ" + }, + "enableTerminal": { + "label": "เปิดใช้งานเทอร์มินัล" + } + } + }, + "palette": { + "localTerminal": "สถานีปลายทางท้องถิ่น" + } +} diff --git a/plugins/ssh-terminal/locales/translated/tr_TR.json b/plugins/ssh-terminal/locales/translated/tr_TR.json new file mode 100644 index 000000000..083c21dea --- /dev/null +++ b/plugins/ssh-terminal/locales/translated/tr_TR.json @@ -0,0 +1,203 @@ +{ + "mobileKeyboard": { + "addQuickKey": "Ekle", + "arrowDown": "Aşağı Ok", + "arrowLeft": "Sol Ok", + "arrowRight": "Sağ Ok", + "arrowUp": "Yukarı Ok", + "backTab": "⇥", + "ctrl": "Ctrl", + "delete": "Del", + "done": "Tamam", + "editQuickKeys": "Hızlı tuşları düzenle", + "end": "End", + "esc": "Esc", + "home": "Home", + "pageDown": "PgDn", + "pageUp": "PgUp", + "paste": "Yapıştır", + "quickKeyPlaceholder": "ör. sudo ", + "quickKeysDesc": "Kaldırmak için × işaretine dokunun. En fazla 8 karakter desteklenir.", + "quickKeysTitle": "Hızlı Tuşlar", + "removeQuickKey": "Kaldır", + "resetDefaults": "Varsayılanlara sıfırla", + "shift": "Shift", + "tab": "Tab" + }, + "terminal": { + "autoTmuxEnableFailed": "Otomatik Tmux etkinleştirme başarısız oldu.", + "autoTmuxEnabled": "{{host}}için otomatik Tmux etkinleştirildi. Bu, sonraki bağlantıdan itibaren geçerli olacak.", + "clipboardHttpWarning": "Panoyu okuyamadım. Bazı tarayıcılar HTTPS dışında panoya erişimi kısıtlar; bunun yerine Ctrl+V tuş kombinasyonunu deneyin veya Termix'i HTTPS üzerinden sunun.", + "closeTab": "Kapat", + "connecting": "Bağlanıyor...", + "connectionLost": "Bağlantı kesildi", + "connectionRejected": "Bağlantı sunucu tarafından reddedildi. Lütfen kimlik doğrulamanızı ve ağ yapılandırmanızı kontrol edin.", + "connectionTimeout": "Bağlantı zaman aşımı", + "enableAutoTmuxAction": "Otomatik Tmux'ı etkinleştirin", + "hostKeyRejected": "SSH sunucu anahtarı doğrulaması reddedildi. Bağlantı iptal edildi.", + "linkDialogCopy": "Kopyala", + "linkDialogOpen": "Aç", + "linkDialogTitle": "Bağlantıyı Aç", + "maxReconnectAttemptsReached": "Maksimum yeniden bağlanma denemesine ulaşıldı", + "noHostSelected": "Hiçbir sunucu seçilmedi", + "opksshAuthFailed": "Kimlik doğrulaması başarısız oldu. Lütfen kimlik bilgilerinizi kontrol edip tekrar deneyin.", + "opksshTimeout": "Kimlik doğrulaması zaman aşımına uğradı. Lütfen tekrar deneyin.", + "passwordPromptFillTitle": "Kayıtlı parola bu isteme doldurulsun mu?", + "reconnect": "Yeniden Bağlan", + "reconnected": "Başarıyla yeniden bağlandı", + "reconnecting": "Yeniden bağlanıyor... ({{attempt}}/{{max}})", + "searchCaseSensitive": "Kibrit Kutusu", + "searchClose": "Kapat (Escape)", + "searchNext": "Sonraki Maç (Giriş)", + "searchNoResults": "Sonuç bulunamadı.", + "searchPlaceholder": "Bulmak", + "searchPrevious": "Önceki Maç (Shift+Enter)", + "searchRegex": "Düzenli İfade Kullanımı", + "searchResultCount": "{{index}} / {{count}}", + "searchWholeWord": "Kelimenin tamamını eşleştir", + "sessionExpiredNotice": "{{host}} üzerindeki önceki oturumunuz {{minutes}} dakika sonra sona erdi; kabuk ve içinde çalışan her şey silindi. Oturumların aktif kalması için bu sunucuda Auto-Tmux'u etkinleştirin.", + "sessionExpiredNoticeNoMinutes": "{{host}} üzerindeki önceki oturumunuz bağlantı kesikken sona erdi; kabuk ve içinde çalışan her şey kayboldu. Oturumların aktif kalması için bu sunucuda Auto-Tmux'u etkinleştirin.", + "sessionTakenOver": "Oturum başka bir sekmede açıldı. Yeniden bağlanılıyor...", + "tmuxAttached": "Bağlı istemciler", + "tmuxAttachedCount": "{{count}} bağlı", + "tmuxCopyHint": "Seçimi ayarlayın ve panoya kopyalamak için Enter tuşuna basın", + "tmuxCreateNew": "Yeni oturum başlat", + "tmuxDetached": "tmux oturumundan ayrıldı", + "tmuxLastActivity": "Son etkinlik", + "tmuxSessionAttached": "tmux oturumuna bağlanıldı: {{name}}", + "tmuxSessionCreated": "tmux oturumu oluşturuldu: {{name}}", + "tmuxSessionPickerDesc": "Bu sunucuda mevcut tmux oturumları bulundu. Yeniden bağlanmak için birini seçin veya yeni bir oturum oluşturun.", + "tmuxSessionPickerTitle": "tmux Oturumları", + "tmuxTimeDays": "{{count}}g önce", + "tmuxTimeHours": "{{count}}s önce", + "tmuxTimeJustNow": "az önce", + "tmuxTimeMinutes": "{{count}}dk önce", + "tmuxUnavailable": "Uzak sunucuda tmux yüklü değil, standart kabuğa dönülüyor", + "tmuxWindowCount": "{{count}} pencere", + "tmuxWindows": "Pencereler", + "unknownError": "Bilinmeyen bir hata oluştu", + "vaultAuthDescription": "HashiCorp Vault'ta oturum açmak için bir pencere açıldı. Orada oturum açmayı tamamlayın; bu bağlantı otomatik olarak devam edecek.", + "vaultAuthFailed": "Vault kimlik doğrulaması başarısız oldu. Lütfen tekrar deneyin.", + "vaultAuthTitle": "Vault Oturum Açması Gerekli", + "vaultReopen": "Oturum açma penceresini yeniden aç", + "websocketAbnormalClose": "Bağlantı beklenmedik şekilde kapandı. Bu, ters proxy veya SSL yapılandırma sorunundan kaynaklanabilir. Lütfen sunucu günlüklerini kontrol edin.", + "websocketError": "WebSocket bağlantı hatası" + }, + "terminalToolbar": { + "detachTmux": "tmux'u ayır", + "detachTmuxDescription": "Tmux oturumunu ayır", + "dismissError": "Görüntü hatasını kapat", + "hideToolbar": "Araç çubuğunu gizle", + "image": "Görüntü işlemleri (yalnızca görüntüler)", + "imageActionFailed": "Görüntü işlemi başarısız oldu", + "layout": "Araç çubuğu görüntüleme modu", + "layoutExpanded": "Ölçütlerle genişletildi", + "layoutIcon": "Yalnızca simge", + "layoutLabeled": "Simgeler ve etiketler", + "moveToolbar": "Araç çubuğunu taşı", + "moveToolbarHint": "Araç çubuğunu yeniden konumlandırmak için sürükleyin.", + "paste": "Resmi yapıştır", + "pasteImage": "Panodan resim yapıştır (yalnızca resimler)", + "retry": "Tekrar dene", + "showToolbar": "Araç çubuğunu genişletin", + "upload": "Resim yükle", + "uploadImage": "Sunucuya bir resim yükleyin.", + "uploadingImage": "Yükleniyor..." + }, + "history": { + "noTerminalSelected": "Terminal seçilmedi", + "noTerminalSelectedHint": "Komut geçmişini görüntülemek için bir SSH terminal sekmesi açın", + "searchPlaceholder": "Geçmişte ara...", + "clearAll": "Tümünü Temizle", + "noHistoryEntries": "Geçmiş girdisi yok", + "trackingDisabled": "Geçmiş takibi kapalı" + }, + "touchInput": { + "enabled": "Dokunmatik Girişi Etkinleştir", + "enabledDesc": "Terminallerde tek parmakla dokunmatik kaydırmaya izin verin.", + "momentumEnabled": "İvme Kazandırın", + "momentumEnabledDesc": "Kaydırma işleminden sonra kısa bir süre daha aşağı kaydırmaya devam edin.", + "advanced": "Gelişmiş", + "resetDefaults": "Varsayılan ayarlara sıfırla", + "dragThreshold": "Sürükleme eşiği", + "maxWheelDelta": "Maksimum tekerlek deltası", + "momentumSampleWindow": "Momentum örnek penceresi", + "releaseGracePeriod": "Serbest bırakma erteleme süresi", + "minimumVelocity": "Minimum hız", + "maximumVelocity": "Maksimum hız", + "maximumDuration": "Maksimum süre", + "maximumTravel": "Maksimum seyahat", + "decayTime": "Bozunma süresi", + "pixelsPerTick": "Piksel başına tik", + "maximumFrameInterval": "Maksimum kare aralığı", + "maximumTicksPerFrame": "Kare başına maksimum tik sayısı" + }, + "imageStorage": { + "instanceId": "Terminal oturum örneği kimliği", + "instanceIdDesc": "Görünürlüğü test etmek için kullanılan, bağlı terminal sekmelerinizden birinin örnek kimliği.", + "selectedModeUnavailable": "Mevcut değil", + "test": "Test", + "testing": "Test...", + "testFailed": "Görüntü depolama testi başarısız oldu.", + "testConnected": "Terminal oturumu bağlandı.", + "testNotConnected": "Bu örnek kimliğiyle bağlantılı bir terminal oturumu bulunmamaktadır.", + "testSelectedMode": "Yükleme işlemi şu şekilde yapılır: {{mode}}", + "testLocalVisible": "Yerel dizin, bağlı sunucudan görülebilir.", + "testLocalNotVisible": "Yerel dizin, bağlı sunucudan görünmez." + }, + "settings": { + "admin": { + "group": { + "touch": "Dokunmatik Giriş", + "images": "Görüntü Depolama" + }, + "sessionTimeoutMinutes": { + "label": "Terminal Oturum Kalıcılığı", + "description": "Bağlantısı kesilmiş bir terminalin sunucuda kapatılmadan önce ne kadar süreyle aktif tutulacağı. Minimum 1 dakika, maksimum 1440 dakika. Bu süreyi aşması gereken oturumlar için, sunucuda Auto-Tmux'u etkinleştirin." + }, + "commandHistoryEnabled": { + "label": "Komut Geçmişi", + "description": "Komut geçmişi kaydına izin ver. Devre dışı bırakıldığında, host başına ayarlara bakılmaksızın geçmiş kaydedilmez." + }, + "imageStorageMode": { + "label": "Depolama Modu", + "description": "Yapıştırılan ve yüklenen terminal görüntülerinin yazıldığı yer. Bağlantı türüne göre otomatik seçim, Yerel ve Uzak (SFTP) ayarları düzeltildi.", + "auto": "Otomatik", + "local": "Yerel", + "remoteSftp": "Uzaktan (SFTP)" + }, + "imageLocalDir": { + "label": "Yerel dizin" + }, + "imageHostPath": { + "label": "Ana bilgisayar yolu", + "description": "Terminal aracıların aldığı yerel dizinin sunucu tarafındaki görünümü." + }, + "imageTtlMs": { + "label": "TTL (ms)" + }, + "imageMaxCount": { + "label": "Maksimum görüntü sayısı" + }, + "imageMaxBytes": { + "label": "Maksimum depolama baytları" + } + }, + "host": { + "enableTerminalToolbar": { + "label": "Terminal Araç Çubuğunu Etkinleştir", + "description": "Etkin terminale sabitlenmiş, bu sunucunun araçlarına ve canlı istatistiklerine hızlı erişim sağlayan bir araç çubuğu gösterin." + }, + "enableCommandHistory": { + "label": "Komut Geçmişi", + "description": "Geçmiş ve otomatik tamamlama için bu terminalde çalıştırılan komutları kaydet" + }, + "enableTerminal": { + "label": "Terminali Etkinleştir" + } + } + }, + "palette": { + "localTerminal": "Yerel Terminal" + } +} diff --git a/plugins/ssh-terminal/locales/translated/uk_UA.json b/plugins/ssh-terminal/locales/translated/uk_UA.json new file mode 100644 index 000000000..cac5de71c --- /dev/null +++ b/plugins/ssh-terminal/locales/translated/uk_UA.json @@ -0,0 +1,203 @@ +{ + "mobileKeyboard": { + "addQuickKey": "Додати", + "arrowDown": "Стрілка вниз", + "arrowLeft": "Стрілка ліворуч", + "arrowRight": "Стрілка праворуч", + "arrowUp": "Стрілка вгору", + "backTab": "⇥", + "ctrl": "Ctrl", + "delete": "Дел", + "done": "Готово", + "editQuickKeys": "Редагувати швидкі клавіші", + "end": "Кінець", + "esc": "Esc", + "home": "Дім", + "pageDown": "ПгДн", + "pageUp": "PgUp", + "paste": "Вставити", + "quickKeyPlaceholder": "наприклад, sudo ", + "quickKeysDesc": "Натисніть ×, щоб видалити. Підтримує до 8 символів.", + "quickKeysTitle": "Швидкі клавіші", + "removeQuickKey": "Видалити", + "resetDefaults": "Скинути до налаштувань за замовчуванням", + "shift": "Зсув", + "tab": "Вкладка" + }, + "terminal": { + "autoTmuxEnableFailed": "Не вдалося ввімкнути Auto-Tmux", + "autoTmuxEnabled": "Автоматичне Tmux-змінення увімкнено для {{host}}. Застосовується з наступного підключення.", + "clipboardHttpWarning": "Не вдалося прочитати буфер обміну. Деякі браузери обмежують доступ до буфера обміну поза HTTPS — спробуйте Ctrl+V або обслуговуйте Termix через HTTPS.", + "closeTab": "Закрити", + "connecting": "Підключення...", + "connectionLost": "З’єднання втрачено", + "connectionRejected": "З’єднання відхилено сервером. Перевірте автентифікацію та конфігурацію мережі.", + "connectionTimeout": "Тайм-аут з'єднання", + "enableAutoTmuxAction": "Увімкнути автоматичне Tmux", + "hostKeyRejected": "Перевірку ключа хоста SSH відхилено. З'єднання скасовано.", + "linkDialogCopy": "Копіювати", + "linkDialogOpen": "ВІДЧИНЕНО", + "linkDialogTitle": "Відкрити посилання", + "maxReconnectAttemptsReached": "Досягнуто максимальної кількості спроб повторного підключення", + "noHostSelected": "Хост не вибрано", + "opksshAuthFailed": "Помилка автентифікації. Перевірте свої облікові дані та спробуйте ще раз.", + "opksshTimeout": "Час очікування автентифікації минув. Спробуйте ще раз.", + "passwordPromptFillTitle": "Введіть збережений пароль у цей запит?", + "reconnect": "Знову підключитися", + "reconnected": "Успішно відновлено підключення", + "reconnecting": "Повторне підключення... ({{attempt}}/{{max}})", + "searchCaseSensitive": "Зіставте регістр", + "searchClose": "Закрити (Escape)", + "searchNext": "Наступний матч (Enter)", + "searchNoResults": "Немає результатів", + "searchPlaceholder": "Знайти", + "searchPrevious": "Попередній збіг (Shift+Enter)", + "searchRegex": "Використовуйте регулярний вираз", + "searchResultCount": "{{index}} з {{count}}", + "searchWholeWord": "Зіставлення цілого слова", + "sessionExpiredNotice": "Ваш попередній сеанс на {{host}} завершився через {{minutes}} хвилин після відключення; оболонка та все, що в ній працює, зникли. Увімкніть Auto-Tmux на цьому хості, щоб підтримувати сеанси активними.", + "sessionExpiredNoticeNoMinutes": "Ваш попередній сеанс на {{host}} закінчився під час відключення; оболонка та все, що в ній працює, зникли. Увімкніть Auto-Tmux на цьому хості, щоб підтримувати сеанси активними.", + "sessionTakenOver": "Сеанс відкрито в іншій вкладці. Повторне підключення...", + "tmuxAttached": "Підключені клієнти", + "tmuxAttachedCount": "{{count}} додається", + "tmuxCopyHint": "Відрегулюйте виділення та натисніть Enter, щоб скопіювати його в буфер обміну", + "tmuxCreateNew": "Почати новий сеанс", + "tmuxDetached": "Від’єднано від сеансу tmux", + "tmuxLastActivity": "Остання активність", + "tmuxSessionAttached": "додано сеанс tmux: {{name}}", + "tmuxSessionCreated": "Створено сеанс tmux: {{name}}", + "tmuxSessionPickerDesc": "На цьому хості знайдено існуючі сеанси tmux. Виберіть один для повторного підключення або створення нового сеансу.", + "tmuxSessionPickerTitle": "Сесії tmux", + "tmuxTimeDays": "{{count}}днів тому", + "tmuxTimeHours": "{{count}}год тому", + "tmuxTimeJustNow": "щойно", + "tmuxTimeMinutes": "{{count}}хв тому", + "tmuxUnavailable": "tmux не встановлено на віддаленому хості, повертається до стандартної оболонки", + "tmuxWindowCount": "вікно {{count}}", + "tmuxWindows": "Вікна", + "unknownError": "Сталася невідома помилка", + "vaultAuthDescription": "Відкрилося вікно для входу в HashiCorp Vault. Завершіть вхід там; це з’єднання буде продовжено автоматично.", + "vaultAuthFailed": "Не вдалося виконати автентифікацію сховища. Спробуйте ще раз.", + "vaultAuthTitle": "Потрібен вхід до Сейфа", + "vaultReopen": "Знову відкрити вікно входу", + "websocketAbnormalClose": "З’єднання несподівано завершилося. Можливо, це пов’язано зі зворотним проксі-сервером або проблемою конфігурації SSL. Перевірте журнали сервера.", + "websocketError": "Помилка підключення WebSocket" + }, + "terminalToolbar": { + "detachTmux": "Від’єднати tmux", + "detachTmuxDescription": "Від’єднати сеанс tmux", + "dismissError": "Помилка відхилення зображення", + "hideToolbar": "Приховати панель інструментів", + "image": "Дії із зображеннями (лише зображення)", + "imageActionFailed": "Не вдалося виконати дію із зображенням", + "layout": "Режим відображення панелі інструментів", + "layoutExpanded": "Розширено показниками", + "layoutIcon": "Тільки значок", + "layoutLabeled": "Піктограми та мітки", + "moveToolbar": "Переміщення панелі інструментів", + "moveToolbarHint": "Перетягніть, щоб змінити положення панелі інструментів", + "paste": "Вставити зображення", + "pasteImage": "Вставити зображення з буфера обміну (лише зображення)", + "retry": "Повторити спробу", + "showToolbar": "Розгорнути панель інструментів", + "upload": "Завантажити зображення", + "uploadImage": "Завантажте зображення на хостинг", + "uploadingImage": "Завантаження..." + }, + "history": { + "noTerminalSelected": "Термінал не вибрано", + "noTerminalSelectedHint": "Відкрийте вкладку терміналу SSH, щоб переглянути історію його команд", + "searchPlaceholder": "Історія пошуку...", + "clearAll": "Очистити все", + "noHistoryEntries": "Немає записів в історії", + "trackingDisabled": "Відстеження історії вимкнено" + }, + "touchInput": { + "enabled": "Увімкнути сенсорне введення", + "enabledDesc": "Дозволити прокручування одним пальцем у терміналах", + "momentumEnabled": "Увімкнути Momentum", + "momentumEnabledDesc": "Продовжуйте коротко гортати після проведення пальцем", + "advanced": "Розширений", + "resetDefaults": "Скинути до налаштувань за замовчуванням", + "dragThreshold": "Поріг перетягування", + "maxWheelDelta": "Максимальна дельта коліс", + "momentumSampleWindow": "Вікно вибірки імпульсу", + "releaseGracePeriod": "Пільговий період випуску", + "minimumVelocity": "Мінімальна швидкість", + "maximumVelocity": "Максимальна швидкість", + "maximumDuration": "Максимальна тривалість", + "maximumTravel": "Максимальний хід", + "decayTime": "Час розпаду", + "pixelsPerTick": "Пікселів на такт", + "maximumFrameInterval": "Максимальний інтервал кадру", + "maximumTicksPerFrame": "Максимальна кількість тиків на кадр" + }, + "imageStorage": { + "instanceId": "Ідентифікатор екземпляра термінального сеансу", + "instanceIdDesc": "Ідентифікатор екземпляра однієї з ваших підключених вкладок терміналу, що використовується для перевірки видимості.", + "selectedModeUnavailable": "Недоступно", + "test": "Тест", + "testing": "Тестування...", + "testFailed": "Не вдалося перевірити сховище зображень", + "testConnected": "Термінальний сеанс підключено.", + "testNotConnected": "Немає підключеного термінального сеансу з цим ідентифікатором екземпляра.", + "testSelectedMode": "Для завантаження буде використано: {{mode}}", + "testLocalVisible": "Локальний каталог видно з підключеного хоста.", + "testLocalNotVisible": "Локальний каталог НЕ видно з підключеного хоста." + }, + "settings": { + "admin": { + "group": { + "touch": "Сенсорний ввід", + "images": "Зберігання зображень" + }, + "sessionTimeoutMinutes": { + "label": "Збереження термінального сеансу", + "description": "Як довго відключений термінал залишається активним на сервері, перш ніж його буде закрито. Мін. 1 · Макс. 1440 хвилин. Для сеансів, які мають тривати довше цього часу, увімкніть Auto-Tmux на хості." + }, + "commandHistoryEnabled": { + "label": "Історія команд", + "description": "Дозволити запис історії команд. Якщо вимкнено, історія не зберігається незалежно від налаштувань для кожного хоста." + }, + "imageStorageMode": { + "label": "Режим зберігання", + "description": "Куди записуються вставлені та завантажені образи терміналів. Виправлено автоматичний вибір за підключенням, локальним та віддаленим (SFTP).", + "auto": "Авто", + "local": "Місцевий", + "remoteSftp": "Віддалений (SFTP)" + }, + "imageLocalDir": { + "label": "Локальний каталог" + }, + "imageHostPath": { + "label": "Шлях хоста", + "description": "Вигляд локального каталогу, який отримують агенти терміналів, з боку хоста." + }, + "imageTtlMs": { + "label": "TTL (мс)" + }, + "imageMaxCount": { + "label": "Максимальна кількість зображень" + }, + "imageMaxBytes": { + "label": "Максимальна кількість байтів пам'яті" + } + }, + "host": { + "enableTerminalToolbar": { + "label": "Увімкнути панель інструментів термінала", + "description": "Показати панель швидкого доступу, прикріплену до активного терміналу, для інструментів цього хоста та актуальної статистики" + }, + "enableCommandHistory": { + "label": "Історія команд", + "description": "Запис команд, що виконуються в цьому терміналі, для історії та автозаповнення" + }, + "enableTerminal": { + "label": "Увімкнути термінал" + } + } + }, + "palette": { + "localTerminal": "Місцевий термінал" + } +} diff --git a/plugins/ssh-terminal/locales/translated/vi_VN.json b/plugins/ssh-terminal/locales/translated/vi_VN.json new file mode 100644 index 000000000..5f6c04b72 --- /dev/null +++ b/plugins/ssh-terminal/locales/translated/vi_VN.json @@ -0,0 +1,203 @@ +{ + "mobileKeyboard": { + "addQuickKey": "Thêm", + "arrowDown": "Mũi tên xuống", + "arrowLeft": "Mũi tên trái", + "arrowRight": "Mũi tên phải", + "arrowUp": "Mũi tên lên", + "backTab": "⇥", + "ctrl": "Ctrl", + "delete": "Del", + "done": "Xong", + "editQuickKeys": "Chỉnh sửa phím tắt", + "end": "End", + "esc": "Esc", + "home": "Home", + "pageDown": "PgDn", + "pageUp": "PgUp", + "paste": "Dán", + "quickKeyPlaceholder": "vd: sudo ", + "quickKeysDesc": "Nhấn × để xóa. Hỗ trợ tối đa 8 ký tự.", + "quickKeysTitle": "Phím tắt", + "removeQuickKey": "Xóa", + "resetDefaults": "Đặt lại mặc định", + "shift": "Shift", + "tab": "Tab" + }, + "terminal": { + "autoTmuxEnableFailed": "Không thể bật Auto-Tmux.", + "autoTmuxEnabled": "Tính năng Auto-Tmux đã được bật cho {{host}}. Tính năng này sẽ được áp dụng từ lần kết nối tiếp theo.", + "clipboardHttpWarning": "Không thể đọc nội dung clipboard. Một số trình duyệt hạn chế quyền truy cập clipboard khi không sử dụng HTTPS - hãy thử Ctrl+V hoặc sử dụng HTTPS để chạy Termix.", + "closeTab": "Đóng", + "connecting": "Đang kết nối...", + "connectionLost": "Mất kết nối", + "connectionRejected": "Kết nối bị máy chủ từ chối. Vui lòng kiểm tra xác thực và cấu hình mạng.", + "connectionTimeout": "Hết thời gian kết nối", + "enableAutoTmuxAction": "Bật Auto-Tmux", + "hostKeyRejected": "Xác minh khóa máy chủ SSH bị từ chối. Đã hủy kết nối.", + "linkDialogCopy": "Sao chép", + "linkDialogOpen": "Mở", + "linkDialogTitle": "Mở liên kết", + "maxReconnectAttemptsReached": "Đã đạt số lần kết nối lại tối đa", + "noHostSelected": "Chưa chọn máy chủ", + "opksshAuthFailed": "Xác thực thất bại. Vui lòng kiểm tra thông tin đăng nhập và thử lại.", + "opksshTimeout": "Xác thực đã hết thời gian chờ. Vui lòng thử lại.", + "passwordPromptFillTitle": "Điền mật khẩu đã lưu vào dòng nhắc này?", + "reconnect": "Kết nối lại", + "reconnected": "Đã kết nối lại thành công", + "reconnecting": "Đang kết nối lại... ({{attempt}}/{{max}})", + "searchCaseSensitive": "Hộp diêm", + "searchClose": "Đóng (Thoát)", + "searchNext": "Trận đấu tiếp theo (Nhập)", + "searchNoResults": "Không có kết quả", + "searchPlaceholder": "Tìm thấy", + "searchPrevious": "Kết quả khớp trước đó (Shift+Enter)", + "searchRegex": "Sử dụng biểu thức chính quy", + "searchResultCount": "{{index}} của {{count}}", + "searchWholeWord": "Ghép toàn bộ từ", + "sessionExpiredNotice": "Phiên làm việc trước của bạn trên {{host}} đã hết hạn sau {{minutes}} phút bị ngắt kết nối; shell và mọi thứ đang chạy trong đó đều đã biến mất. Hãy bật Auto-Tmux trên máy chủ này để duy trì các phiên làm việc.", + "sessionExpiredNoticeNoMinutes": "Phiên làm việc trước của bạn trên {{host}} đã hết hạn khi bị ngắt kết nối; shell và mọi thứ đang chạy trong đó đều đã biến mất. Hãy bật Auto-Tmux trên máy chủ này để duy trì các phiên làm việc.", + "sessionTakenOver": "Phiên đã được mở trong tab khác. Đang kết nối lại...", + "tmuxAttached": "Các máy khách đã gắn kết", + "tmuxAttachedCount": "{{count}} đã gắn kết", + "tmuxCopyHint": "Điều chỉnh lựa chọn và nhấn Enter để sao chép vào clipboard", + "tmuxCreateNew": "Tạo phiên mới", + "tmuxDetached": "Đã ngắt khỏi phiên tmux", + "tmuxLastActivity": "Hoạt động cuối", + "tmuxSessionAttached": "Đã gắn vào phiên tmux: {{name}}", + "tmuxSessionCreated": "Đã tạo phiên tmux: {{name}}", + "tmuxSessionPickerDesc": "Các phiên tmux hiện có trên máy chủ này. Chọn một phiên để gắn lại hoặc tạo phiên mới.", + "tmuxSessionPickerTitle": "Phiên tmux", + "tmuxTimeDays": "{{count}} ngày trước", + "tmuxTimeHours": "{{count}} giờ trước", + "tmuxTimeJustNow": "vừa xong", + "tmuxTimeMinutes": "{{count}} phút trước", + "tmuxUnavailable": "tmux chưa được cài đặt trên máy chủ từ xa, chuyển sang shell tiêu chuẩn", + "tmuxWindowCount": "{{count}} cửa sổ", + "tmuxWindows": "Cửa sổ", + "unknownError": "Lỗi không xác định đã xảy ra", + "vaultAuthDescription": "Một cửa sổ đã mở để đăng nhập vào HashiCorp Vault. Hoàn tất đăng nhập ở đó; kết nối này sẽ tự động tiếp tục.", + "vaultAuthFailed": "Xác thực Vault thất bại. Vui lòng thử lại.", + "vaultAuthTitle": "Yêu cầu đăng nhập Vault", + "vaultReopen": "Mở lại cửa sổ đăng nhập", + "websocketAbnormalClose": "Kết nối đóng đột ngột. Có thể do sự cố cấu hình reverse proxy hoặc SSL. Vui lòng kiểm tra nhật ký máy chủ.", + "websocketError": "Lỗi kết nối WebSocket" + }, + "terminalToolbar": { + "detachTmux": "Tách tmux", + "detachTmuxDescription": "Tách phiên tmux", + "dismissError": "Bỏ qua lỗi hình ảnh", + "hideToolbar": "Ẩn thanh công cụ", + "image": "Thao tác với hình ảnh (chỉ áp dụng cho hình ảnh)", + "imageActionFailed": "Thao tác xử lý hình ảnh thất bại", + "layout": "Chế độ hiển thị thanh công cụ", + "layoutExpanded": "Đã mở rộng với các số liệu", + "layoutIcon": "Chỉ có biểu tượng", + "layoutLabeled": "Biểu tượng và nhãn", + "moveToolbar": "Di chuyển thanh công cụ", + "moveToolbarHint": "Kéo để định vị lại thanh công cụ", + "paste": "Dán hình ảnh", + "pasteImage": "Dán hình ảnh từ clipboard (chỉ áp dụng cho hình ảnh)", + "retry": "Thử lại", + "showToolbar": "Mở rộng thanh công cụ", + "upload": "Tải ảnh lên", + "uploadImage": "Tải ảnh lên máy chủ", + "uploadingImage": "Đang tải lên..." + }, + "history": { + "noTerminalSelected": "Chưa chọn terminal", + "noTerminalSelectedHint": "Mở một tab terminal SSH để xem lịch sử lệnh", + "searchPlaceholder": "Tìm kiếm lịch sử...", + "clearAll": "Xóa tất cả", + "noHistoryEntries": "Không có mục lịch sử", + "trackingDisabled": "Tính năng ghi lịch sử đã bị tắt" + }, + "touchInput": { + "enabled": "Cho phép nhập liệu cảm ứng", + "enabledDesc": "Cho phép cuộn bằng một ngón tay trong cửa sổ dòng lệnh.", + "momentumEnabled": "Tạo đà", + "momentumEnabledDesc": "Tiếp tục cuộn xuống một chút sau khi vuốt.", + "advanced": "Trình độ cao", + "resetDefaults": "Khôi phục về cài đặt mặc định", + "dragThreshold": "Ngưỡng kéo", + "maxWheelDelta": "Chênh lệch bánh xe tối đa", + "momentumSampleWindow": "Cửa sổ mẫu động lượng", + "releaseGracePeriod": "thời gian ân hạn thả", + "minimumVelocity": "Vận tốc tối thiểu", + "maximumVelocity": "Vận tốc tối đa", + "maximumDuration": "Thời gian tối đa", + "maximumTravel": "Hành trình tối đa", + "decayTime": "Thời gian phân rã", + "pixelsPerTick": "Số pixel trên mỗi tick", + "maximumFrameInterval": "Khoảng thời gian khung hình tối đa", + "maximumTicksPerFrame": "Số tick tối đa mỗi khung hình" + }, + "imageStorage": { + "instanceId": "ID phiên bản phiên thiết bị đầu cuối", + "instanceIdDesc": "ID phiên bản của một trong các tab thiết bị đầu cuối được kết nối của bạn, được sử dụng để kiểm tra khả năng hiển thị.", + "selectedModeUnavailable": "Không khả dụng", + "test": "Bài kiểm tra", + "testing": "Đang thử nghiệm...", + "testFailed": "Không thể kiểm tra lưu trữ hình ảnh", + "testConnected": "Phiên kết nối terminal đã được thiết lập.", + "testNotConnected": "Không có phiên làm việc thiết bị đầu cuối nào được kết nối với ID phiên bản này.", + "testSelectedMode": "Quá trình tải lên sẽ sử dụng: {{mode}}", + "testLocalVisible": "Thư mục cục bộ có thể được nhìn thấy từ máy chủ được kết nối.", + "testLocalNotVisible": "Thư mục cục bộ KHÔNG hiển thị từ máy chủ được kết nối." + }, + "settings": { + "admin": { + "group": { + "touch": "Nhập liệu cảm ứng", + "images": "Lưu trữ hình ảnh" + }, + "sessionTimeoutMinutes": { + "label": "Duy trì phiên đầu cuối", + "description": "Thời gian duy trì kết nối của một thiết bị đầu cuối bị ngắt kết nối trên máy chủ trước khi nó bị đóng. Tối thiểu 1 phút · Tối đa 1440 phút. Đối với các phiên cần duy trì lâu hơn thời gian này, hãy bật Auto-Tmux trên máy chủ." + }, + "commandHistoryEnabled": { + "label": "Lịch sử lệnh", + "description": "Cho phép ghi lịch sử lệnh. Khi tắt, lịch sử sẽ không được lưu bất kể cài đặt từng máy chủ." + }, + "imageStorageMode": { + "label": "Chế độ lưu trữ", + "description": "Nơi hiển thị hình ảnh từ thiết bị đầu cuối được dán và tải lên. Chức năng tự động chọn theo kết nối, cục bộ và từ xa (SFTP) được cố định.", + "auto": "Tự động", + "local": "Địa phương", + "remoteSftp": "Từ xa (SFTP)" + }, + "imageLocalDir": { + "label": "Thư mục cục bộ" + }, + "imageHostPath": { + "label": "Đường dẫn máy chủ", + "description": "Đây là giao diện phía máy chủ của thư mục cục bộ mà các tác nhân đầu cuối nhận được." + }, + "imageTtlMs": { + "label": "TTL (ms)" + }, + "imageMaxCount": { + "label": "Số lượng ảnh tối đa" + }, + "imageMaxBytes": { + "label": "Số byte lưu trữ tối đa" + } + }, + "host": { + "enableTerminalToolbar": { + "label": "Kích hoạt thanh công cụ Terminal", + "description": "Hiển thị thanh công cụ truy cập nhanh được gắn vào cửa sổ terminal đang hoạt động để xem các công cụ và số liệu thống kê trực tiếp của máy chủ này." + }, + "enableCommandHistory": { + "label": "Lịch sử lệnh", + "description": "Ghi lại các lệnh chạy trong terminal này để dùng cho lịch sử và tự động hoàn thành" + }, + "enableTerminal": { + "label": "Bật Terminal" + } + } + }, + "palette": { + "localTerminal": "Ga địa phương" + } +} diff --git a/plugins/ssh-terminal/locales/translated/zh_CN.json b/plugins/ssh-terminal/locales/translated/zh_CN.json new file mode 100644 index 000000000..bc2299e8e --- /dev/null +++ b/plugins/ssh-terminal/locales/translated/zh_CN.json @@ -0,0 +1,203 @@ +{ + "mobileKeyboard": { + "addQuickKey": "添加", + "arrowDown": "Arrow Down", + "arrowLeft": "Arrow Left", + "arrowRight": "Arrow Right", + "arrowUp": "Arrow Up", + "backTab": "⇥", + "ctrl": "Ctrl", + "delete": "Del", + "done": "完成", + "editQuickKeys": "编辑快速键", + "end": "End", + "esc": "Esc", + "home": "Home", + "pageDown": "PgDn", + "pageUp": "PgUp", + "paste": "粘贴", + "quickKeyPlaceholder": "例如 sudo ", + "quickKeysDesc": "点击 × 删除。最多支持 8 个字符。", + "quickKeysTitle": "快速键", + "removeQuickKey": "删除", + "resetDefaults": "重置为默认值", + "shift": "Shift", + "tab": "Tab" + }, + "terminal": { + "autoTmuxEnableFailed": "启用自动 Tmux 失败", + "autoTmuxEnabled": "对 {{host}}启用自动 Tmux。它从下一个连接开始生效。", + "clipboardHttpWarning": "无法读取剪贴板内容。部分浏览器限制在 HTTPS 协议之外访问剪贴板——请尝试使用 Ctrl+V 粘贴,或通过 HTTPS 协议访问 Termix。", + "closeTab": "关闭", + "connecting": "正在连接…", + "connectionLost": "连接丢失", + "connectionRejected": "服务器拒绝了连接。请检查您的身份验证和网络配置。", + "connectionTimeout": "连接超时", + "enableAutoTmuxAction": "启用自动 Tmux", + "hostKeyRejected": "SSH主机密钥验证被拒绝。连接已取消。", + "linkDialogCopy": "复制", + "linkDialogOpen": "打开", + "linkDialogTitle": "打开链接", + "maxReconnectAttemptsReached": "已达到最大重连尝试次数", + "noHostSelected": "未选择主机", + "opksshAuthFailed": "认证失败,请检查凭据并重试。", + "opksshTimeout": "认证超时,请重试。", + "passwordPromptFillTitle": "将保存的密码填入此提示?", + "reconnect": "重新连接", + "reconnected": "重新连接成功", + "reconnecting": "正在重新连接…({{attempt}}/{{max}})", + "searchCaseSensitive": "区分大小写", + "searchClose": "关闭(Esc)", + "searchNext": "下一个匹配(Enter)", + "searchNoResults": "没有结果", + "searchPlaceholder": "搜索", + "searchPrevious": "上一个匹配(Shift+Enter)", + "searchRegex": "使用正则表达式", + "searchResultCount": "{{index}} 的 {{count}}", + "searchWholeWord": "全字匹配", + "sessionExpiredNotice": "您之前在 {{host}} 上的会话已在 {{minutes}} 分钟后断开连接并过期;shell 及其中运行的所有内容均已丢失。请在此主机上启用 Auto-Tmux 以保持会话连接。", + "sessionExpiredNoticeNoMinutes": "您之前在 {{host}} 上的会话已过期,且已断开连接;shell 及其中运行的所有内容均已丢失。请在此主机上启用 Auto-Tmux 以保持会话有效。", + "sessionTakenOver": "会话已在另一个标签页中打开。正在重新连接...", + "tmuxAttached": "已连接客户端", + "tmuxAttachedCount": "{{count}} 个已连接", + "tmuxCopyHint": "调整选择并按 Enter 复制到剪贴板", + "tmuxCreateNew": "启动新会话", + "tmuxDetached": "已从 tmux 会话分离", + "tmuxLastActivity": "最后活动", + "tmuxSessionAttached": "tmux 会话已附加:{{name}}", + "tmuxSessionCreated": "tmux 会话已创建:{{name}}", + "tmuxSessionPickerDesc": "在此主机上发现现有 tmux 会话。选择一个重新连接或创建新会话。", + "tmuxSessionPickerTitle": "tmux 会话", + "tmuxTimeDays": "{{count}} 天前", + "tmuxTimeHours": "{{count}} 小时前", + "tmuxTimeJustNow": "刚刚", + "tmuxTimeMinutes": "{{count}} 分钟前", + "tmuxUnavailable": "远程主机上未安装 tmux,回退到标准 shell", + "tmuxWindowCount": "{{count}} 个窗口", + "tmuxWindows": "窗口", + "unknownError": "发生未知错误", + "vaultAuthDescription": "已打开窗口以登录 HashiCorp Vault。在该处完成登录;此连接将自动继续。", + "vaultAuthFailed": "Vault 认证失败,请重试。", + "vaultAuthTitle": "需要 Vault 登录", + "vaultReopen": "重新打开登录窗口", + "websocketAbnormalClose": "连接意外关闭。这可能是因为反向代理或SSL配置问题。请检查服务器日志。", + "websocketError": "WebSocket 连接错误" + }, + "terminalToolbar": { + "detachTmux": "分离 tmux", + "detachTmuxDescription": "分离 tmux 会话", + "dismissError": "忽略图像错误", + "hideToolbar": "隐藏工具栏", + "image": "图片操作(仅限图片)", + "imageActionFailed": "图片操作失败", + "layout": "工具栏显示模式", + "layoutExpanded": "扩展了指标", + "layoutIcon": "仅图标", + "layoutLabeled": "图标和标签", + "moveToolbar": "移动工具栏", + "moveToolbarHint": "拖动即可重新定位工具栏", + "paste": "粘贴图片", + "pasteImage": "从剪贴板粘贴图片(仅限图片)", + "retry": "重试", + "showToolbar": "展开工具栏", + "upload": "上传图片", + "uploadImage": "将图片上传到主机", + "uploadingImage": "正在上传..." + }, + "history": { + "noTerminalSelected": "未选择终端", + "noTerminalSelectedHint": "打开一个 SSH 终端标签页以查看其命令历史", + "searchPlaceholder": "搜索历史...", + "clearAll": "全部清除", + "noHistoryEntries": "无历史记录", + "trackingDisabled": "历史跟踪已禁用" + }, + "touchInput": { + "enabled": "启用触摸输入", + "enabledDesc": "允许终端单指触摸滚动", + "momentumEnabled": "启用动能", + "momentumEnabledDesc": "滑动后继续短暂滚动。", + "advanced": "高级", + "resetDefaults": "恢复默认设置", + "dragThreshold": "拖动阈值", + "maxWheelDelta": "最大滚轮增量", + "momentumSampleWindow": "动量样本窗口", + "releaseGracePeriod": "释放宽限期", + "minimumVelocity": "最小速度", + "maximumVelocity": "最大速度", + "maximumDuration": "最长持续时间", + "maximumTravel": "最大行程", + "decayTime": "衰减时间", + "pixelsPerTick": "每刻像素数", + "maximumFrameInterval": "最大帧间隔", + "maximumTicksPerFrame": "每帧最大刻度数" + }, + "imageStorage": { + "instanceId": "终端会话实例 ID", + "instanceIdDesc": "用于测试可见性的已连接终端标签页的实例 ID。", + "selectedModeUnavailable": "不可用", + "test": "测试", + "testing": "测试中……", + "testFailed": "图像存储测试失败", + "testConnected": "终端会话已连接。", + "testNotConnected": "没有与此实例 ID 连接的终端会话。", + "testSelectedMode": "上传将使用:{{mode}}", + "testLocalVisible": "本地目录对连接的主机可见。", + "testLocalNotVisible": "从连接的主机上不可见本地目录。" + }, + "settings": { + "admin": { + "group": { + "touch": "触摸输入", + "images": "图像存储" + }, + "sessionTimeoutMinutes": { + "label": "终端会话持久性", + "description": "断开连接的终端在服务器上保持活动状态的最长时间。最短 1 分钟,最长 1440 分钟。对于必须保持活动时间超过此限制的会话,请在主机上启用自动终端复用 (Auto-Tmux)。" + }, + "commandHistoryEnabled": { + "label": "命令历史", + "description": "允许记录命令历史。禁用后,无论各主机设置如何,均不保存历史记录。" + }, + "imageStorageMode": { + "label": "存储模式", + "description": "粘贴和上传的终端图片的写入位置。自动模式根据连接自动选择,本地和远程(SFTP)为固定模式。", + "auto": "自动", + "local": "本地", + "remoteSftp": "远程(SFTP)" + }, + "imageLocalDir": { + "label": "本地目录" + }, + "imageHostPath": { + "label": "主机路径", + "description": "终端代理接收到的本地目录的主机端视图。" + }, + "imageTtlMs": { + "label": "TTL(毫秒)" + }, + "imageMaxCount": { + "label": "最大图像数量" + }, + "imageMaxBytes": { + "label": "最大存储字节" + } + }, + "host": { + "enableTerminalToolbar": { + "label": "启用终端工具栏", + "description": "在当前终端上显示该主机的工具和实时信息的 Dock栏" + }, + "enableCommandHistory": { + "label": "命令历史", + "description": "记录此终端中运行的命令,用于历史记录和自动补全" + }, + "enableTerminal": { + "label": "启用终端" + } + } + }, + "palette": { + "localTerminal": "本地终端" + } +} diff --git a/plugins/ssh-terminal/locales/translated/zh_TW.json b/plugins/ssh-terminal/locales/translated/zh_TW.json new file mode 100644 index 000000000..c9e27cdec --- /dev/null +++ b/plugins/ssh-terminal/locales/translated/zh_TW.json @@ -0,0 +1,203 @@ +{ + "mobileKeyboard": { + "addQuickKey": "新增", + "arrowDown": "下箭頭", + "arrowLeft": "左箭頭", + "arrowRight": "右箭頭", + "arrowUp": "上箭頭", + "backTab": "⇥", + "ctrl": "Ctrl", + "delete": "Del", + "done": "完成", + "editQuickKeys": "編輯快捷鍵", + "end": "End", + "esc": "Esc", + "home": "Home", + "pageDown": "PgDn", + "pageUp": "PgUp", + "paste": "貼上", + "quickKeyPlaceholder": "例如:sudo ", + "quickKeysDesc": "點選 × 來移除。最多支援 8 個字元。", + "quickKeysTitle": "快捷鍵", + "removeQuickKey": "移除", + "resetDefaults": "重設為預設值", + "shift": "Shift", + "tab": "Tab" + }, + "terminal": { + "autoTmuxEnableFailed": "啟用自動 Tmux 失敗", + "autoTmuxEnabled": "對 {{host}}啟用自動 Tmux。它從下一個連接開始生效。", + "clipboardHttpWarning": "無法讀取剪貼簿內容。部分瀏覽器限制在 HTTPS 協定之外存取剪貼簿內容 - 請嘗試使用 Ctrl+V 貼上,或透過 HTTPS 協定存取 Termix。", + "closeTab": "關閉", + "connecting": "連線中...", + "connectionLost": "連線中斷", + "connectionRejected": "連線被伺服器拒絕。請檢查您的驗證與網路設定。", + "connectionTimeout": "連線逾時", + "enableAutoTmuxAction": "啟用自動 Tmux", + "hostKeyRejected": "SSH 主機金鑰驗證被拒絕。連線已取消。", + "linkDialogCopy": "複製", + "linkDialogOpen": "開啟", + "linkDialogTitle": "開啟連結", + "maxReconnectAttemptsReached": "已達最大重新連線嘗試次數", + "noHostSelected": "未選取主機", + "opksshAuthFailed": "驗證失敗。請檢查您的憑證並再試一次。", + "opksshTimeout": "驗證逾時。請再試一次。", + "passwordPromptFillTitle": "將已儲存的密碼填入此提示?", + "reconnect": "重新連線", + "reconnected": "已成功重新連線", + "reconnecting": "重新連線中... ({{attempt}}/{{max}})", + "searchCaseSensitive": "火柴盒", + "searchClose": "關閉(退出)", + "searchNext": "下一場比賽(輸入)", + "searchNoResults": "沒有結果", + "searchPlaceholder": "尋找", + "searchPrevious": "最後一場比賽(Shift+Enter)", + "searchRegex": "使用正規表示式", + "searchResultCount": "{{index}} 的 {{count}}", + "searchWholeWord": "整詞匹配", + "sessionExpiredNotice": "您之前在 {{host}} 上的會話已在 {{minutes}} 分鐘後斷開連接並過期;shell 及其中運行的所有內容均已丟失。請在此主機上啟用 Auto-Tmux 以保持會話連線。", + "sessionExpiredNoticeNoMinutes": "您先前在 {{host}} 上的會話已過期,且已斷線;shell 及其中執行的所有內容均已遺失。請在此主機上啟用 Auto-Tmux 以保持會話有效。", + "sessionTakenOver": "工作階段已在另一個分頁中開啟。正在重新連線…", + "tmuxAttached": "已附加的用戶端", + "tmuxAttachedCount": "{{count}} 個已附加", + "tmuxCopyHint": "調整選取範圍並按下 Enter 以複製到剪貼簿", + "tmuxCreateNew": "開始新的工作階段", + "tmuxDetached": "已從 tmux 工作階段中斷連線", + "tmuxLastActivity": "上次活動", + "tmuxSessionAttached": "tmux 工作階段已附加:{{name}}", + "tmuxSessionCreated": "tmux 工作階段已建立:{{name}}", + "tmuxSessionPickerDesc": "在此主機上找到現有的 tmux 工作階段。選擇一個以重新附加或建立新的工作階段。", + "tmuxSessionPickerTitle": "tmux 工作階段", + "tmuxTimeDays": "{{count}} 天前", + "tmuxTimeHours": "{{count}} 小時前", + "tmuxTimeJustNow": "剛剛", + "tmuxTimeMinutes": "{{count}} 分鐘前", + "tmuxUnavailable": "遠端主機未安裝 tmux,切換回標準 shell", + "tmuxWindowCount": "{{count}} 個視窗", + "tmuxWindows": "視窗", + "unknownError": "發生未知錯誤", + "vaultAuthDescription": "已開啟視窗以登入 HashiCorp Vault。在該處完成登入;此連線將自動繼續。", + "vaultAuthFailed": "Vault 驗證失敗。請再試一次。", + "vaultAuthTitle": "需要 Vault 登入", + "vaultReopen": "重新開啟登入視窗", + "websocketAbnormalClose": "連線意外關閉。這可能是由於反向代理或 SSL 設定問題所導致。請檢查伺服器記錄。", + "websocketError": "WebSocket 連線錯誤" + }, + "terminalToolbar": { + "detachTmux": "分離 tmux", + "detachTmuxDescription": "分離 tmux 會話", + "dismissError": "忽略圖像錯誤", + "hideToolbar": "隱藏工具列", + "image": "圖片操作(僅限圖片)", + "imageActionFailed": "圖片操作失敗", + "layout": "工具列顯示模式", + "layoutExpanded": "擴展了指標", + "layoutIcon": "僅圖示", + "layoutLabeled": "圖標和標籤", + "moveToolbar": "移動工具列", + "moveToolbarHint": "拖曳即可重新定位工具列", + "paste": "貼上圖片", + "pasteImage": "從剪貼簿貼上圖片(僅限圖片)", + "retry": "重試", + "showToolbar": "展開工具列", + "upload": "上傳圖片", + "uploadImage": "將圖片上傳到主機", + "uploadingImage": "正在上傳..." + }, + "history": { + "noTerminalSelected": "未選取終端機", + "noTerminalSelectedHint": "開啟 SSH 終端機分頁以檢視其命令歷史記錄", + "searchPlaceholder": "搜尋歷史記錄...", + "clearAll": "全部清除", + "noHistoryEntries": "無歷史記錄項目", + "trackingDisabled": "歷史記錄追蹤已停用" + }, + "touchInput": { + "enabled": "啟用觸控輸入", + "enabledDesc": "允許終端單指觸摸滾動", + "momentumEnabled": "啟用動能", + "momentumEnabledDesc": "滑動後繼續短暫滾動。", + "advanced": "先進的", + "resetDefaults": "恢復預設設定", + "dragThreshold": "阻力閾值", + "maxWheelDelta": "最大車輪增量", + "momentumSampleWindow": "動量樣本窗口", + "releaseGracePeriod": "發布寬限期", + "minimumVelocity": "最低速度", + "maximumVelocity": "最大速度", + "maximumDuration": "最長持續時間", + "maximumTravel": "最大行程", + "decayTime": "衰減時間", + "pixelsPerTick": "每刻像素數", + "maximumFrameInterval": "最大訊框間隔", + "maximumTicksPerFrame": "每格最大幀數" + }, + "imageStorage": { + "instanceId": "終端機會話實例 ID", + "instanceIdDesc": "用於測試可見性的已連接終端選項卡的實例 ID。", + "selectedModeUnavailable": "不可用", + "test": "測試", + "testing": "測試中…", + "testFailed": "影像儲存測試失敗", + "testConnected": "終端會話已連線。", + "testNotConnected": "沒有與此實例 ID 連線的終端會話。", + "testSelectedMode": "上傳時請使用: {{mode}}", + "testLocalVisible": "本機目錄對連接的主機可見。", + "testLocalNotVisible": "從連線的主機上不可見本機目錄。" + }, + "settings": { + "admin": { + "group": { + "touch": "觸摸輸入", + "images": "影像儲存" + }, + "sessionTimeoutMinutes": { + "label": "終端會話持久性", + "description": "斷開連線的終端機在伺服器上保持活動狀態的最長時間。最短 1 分鐘,最長 1440 分鐘。對於必須保持活動時間超過此限制的會話,請在主機上啟用自動終端機重複使用 (Auto-Tmux)。" + }, + "commandHistoryEnabled": { + "label": "指令歷史記錄", + "description": "允許記錄指令歷史記錄。停用時,無論各主機設定如何,都不會儲存歷史記錄。" + }, + "imageStorageMode": { + "label": "儲存模式", + "description": "終端鏡像貼上和上傳位置已設定。自動選擇連接、本機和遠端(SFTP)鏡像的功能已修復。", + "auto": "汽車", + "local": "當地的", + "remoteSftp": "遠端(SFTP)" + }, + "imageLocalDir": { + "label": "本地目錄" + }, + "imageHostPath": { + "label": "主機路徑", + "description": "終端代理接收到的本機目錄的主機端視圖。" + }, + "imageTtlMs": { + "label": "TTL(毫秒)" + }, + "imageMaxCount": { + "label": "最大影像數量" + }, + "imageMaxBytes": { + "label": "最大儲存位元組" + } + }, + "host": { + "enableTerminalToolbar": { + "label": "啟用終端機工具列", + "description": "在目前終端上顯示一個快速存取工具列,用於存取該主機的工具和即時統計資訊。" + }, + "enableCommandHistory": { + "label": "命令歷史記錄", + "description": "記錄在此終端機中執行的命令,用於歷史記錄與自動完成" + }, + "enableTerminal": { + "label": "啟用終端機" + } + } + }, + "palette": { + "localTerminal": "本地端" + } +} diff --git a/plugins/ssh-terminal/manifest.json b/plugins/ssh-terminal/manifest.json index b9c5de326..51c16489e 100644 --- a/plugins/ssh-terminal/manifest.json +++ b/plugins/ssh-terminal/manifest.json @@ -2,7 +2,7 @@ "id": "ssh-terminal", "name": "SSH Terminal", "version": "1.0.0", - "description": "Interactive SSH terminal sessions, with jump hosts, host key verification and session sharing.", + "description": "Interactive SSH terminal sessions, with jump hosts, host key verification, command history and the desktop app's local terminal.", "author": { "name": "Termix" }, @@ -19,8 +19,39 @@ "credentials:use", "ssh:connect", "network:serve", + "db:own", + "events:core", "ui:surface" ], + "provides": [ + { + "service": "sessions.live", + "version": "1.0.0", + "permission": "ssh-terminal.sessions" + }, + { + "service": "terminal.history", + "version": "1.0.0", + "permission": "ssh-terminal.history" + } + ], + "requires": [ + { + "service": "tmux.sessions", + "versionRange": "^1.0.0", + "optional": true + }, + { + "service": "sessions.sharing", + "versionRange": "^1.0.0", + "optional": true + }, + { + "service": "recordings.writer", + "versionRange": "^1.0.0", + "optional": true + } + ], "contributes": { "tabs": [ { @@ -28,6 +59,19 @@ "titleKey": "nav.terminal", "icon": "SquareTerminal", "openFrom": ["host-context-menu", "palette"] + }, + { + "id": "local-terminal", + "titleKey": "nav.localTerminal", + "icon": "Laptop", + "openFrom": ["rail", "palette"] + } + ], + "panels": [ + { + "id": "history", + "titleKey": "nav.history", + "icon": "History" } ], "actionSlots": [ @@ -36,14 +80,160 @@ "accepts": ["button"] }, { - "id": "terminal.dock", + "id": "terminal.toolbarStatus", + "accepts": ["component"] + }, + { + "id": "terminal.sidePanel", "accepts": ["component"] }, { "id": "terminal.overlay", "accepts": ["component"] } - ] + ], + "permissions": [ + { + "name": "sessions", + "titleKey": "permissions.sessions.title", + "descriptionKey": "permissions.sessions.description", + "defaultRoles": ["admin", "user"] + }, + { + "name": "history", + "titleKey": "permissions.history.title", + "descriptionKey": "permissions.history.description", + "defaultRoles": ["admin", "user"] + } + ], + "settings": { + "admin": [ + { + "key": "sessionTimeoutMinutes", + "type": "number", + "labelKey": "settings.admin.sessionTimeoutMinutes.label", + "descriptionKey": "settings.admin.sessionTimeoutMinutes.description", + "group": "settings.admin.group.sessions", + "min": 1, + "max": 1440, + "default": 30 + }, + { + "key": "sessionPersistence", + "type": "boolean", + "labelKey": "settings.admin.sessionPersistence.label", + "descriptionKey": "settings.admin.sessionPersistence.description", + "group": "settings.admin.group.sessions", + "default": true + }, + { + "key": "commandHistoryEnabled", + "type": "boolean", + "labelKey": "settings.admin.commandHistoryEnabled.label", + "descriptionKey": "settings.admin.commandHistoryEnabled.description", + "group": "settings.admin.group.sessions", + "default": true + }, + { + "key": "touchInput", + "type": "custom", + "component": "touchInput", + "group": "settings.admin.group.touch" + }, + { + "key": "imageStorageMode", + "type": "select", + "labelKey": "settings.admin.imageStorageMode.label", + "descriptionKey": "settings.admin.imageStorageMode.description", + "group": "settings.admin.group.images", + "options": [ + { + "value": "auto", + "labelKey": "settings.admin.imageStorageMode.auto" + }, + { + "value": "local", + "labelKey": "settings.admin.imageStorageMode.local" + }, + { + "value": "remote-sftp", + "labelKey": "settings.admin.imageStorageMode.remoteSftp" + } + ] + }, + { + "key": "imageLocalDir", + "type": "secret", + "labelKey": "settings.admin.imageLocalDir.label", + "descriptionKey": "settings.admin.imageLocalDir.description", + "group": "settings.admin.group.images" + }, + { + "key": "imageHostPath", + "type": "string", + "labelKey": "settings.admin.imageHostPath.label", + "descriptionKey": "settings.admin.imageHostPath.description", + "group": "settings.admin.group.images" + }, + { + "key": "imageTtlMs", + "type": "number", + "labelKey": "settings.admin.imageTtlMs.label", + "descriptionKey": "settings.admin.imageTtlMs.description", + "group": "settings.admin.group.images", + "min": 0 + }, + { + "key": "imageMaxCount", + "type": "number", + "labelKey": "settings.admin.imageMaxCount.label", + "descriptionKey": "settings.admin.imageMaxCount.description", + "group": "settings.admin.group.images", + "min": 1 + }, + { + "key": "imageMaxBytes", + "type": "number", + "labelKey": "settings.admin.imageMaxBytes.label", + "descriptionKey": "settings.admin.imageMaxBytes.description", + "group": "settings.admin.group.images", + "min": 1048576 + }, + { + "key": "imageStorageTest", + "type": "custom", + "component": "imageStorageTest", + "group": "settings.admin.group.images" + } + ], + "host": { + "fields": [ + { + "key": "enableTerminal", + "type": "boolean", + "labelKey": "settings.host.enableTerminal.label", + "descriptionKey": "settings.host.enableTerminal.description", + "default": true + }, + { + "key": "enableTerminalToolbar", + "type": "boolean", + "labelKey": "settings.host.enableTerminalToolbar.label", + "descriptionKey": "settings.host.enableTerminalToolbar.description", + "requires": "enableTerminal", + "default": true + }, + { + "key": "enableCommandHistory", + "type": "boolean", + "labelKey": "settings.host.enableCommandHistory.label", + "descriptionKey": "settings.host.enableCommandHistory.description", + "requires": "enableTerminal", + "default": true + } + ] + } + } }, "locales": "locales", "platforms": ["linux", "win32", "darwin"] diff --git a/plugins/ssh-terminal/migrations/mysql/0001_adopt_command_history.sql b/plugins/ssh-terminal/migrations/mysql/0001_adopt_command_history.sql new file mode 100644 index 000000000..ca3e5f6eb --- /dev/null +++ b/plugins/ssh-terminal/migrations/mysql/0001_adopt_command_history.sql @@ -0,0 +1,13 @@ +-- ssh-terminal 0001: adopt_command_history +-- Generated by termix-plugin migrations. Review before committing. + +CREATE TABLE IF NOT EXISTS `command_history` ( + `id` int AUTO_INCREMENT PRIMARY KEY, + `user_id` varchar(255) NOT NULL, + `host_id` int NOT NULL, + `command` text NOT NULL, + `executed_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE, + FOREIGN KEY (`host_id`) REFERENCES `ssh_data` (`id`) ON DELETE CASCADE +); +RENAME TABLE `command_history` TO `p_ssh_terminal_command_history`; diff --git a/plugins/ssh-terminal/migrations/postgres/0001_adopt_command_history.sql b/plugins/ssh-terminal/migrations/postgres/0001_adopt_command_history.sql new file mode 100644 index 000000000..11d93383d --- /dev/null +++ b/plugins/ssh-terminal/migrations/postgres/0001_adopt_command_history.sql @@ -0,0 +1,14 @@ +-- ssh-terminal 0001: adopt_command_history +-- Generated by termix-plugin migrations. Review before committing. + +CREATE TABLE IF NOT EXISTS "command_history" ( + "id" serial PRIMARY KEY, + "user_id" varchar(255) NOT NULL, + "host_id" integer NOT NULL, + "command" text NOT NULL, + "executed_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE, + FOREIGN KEY ("host_id") REFERENCES "ssh_data" ("id") ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS "idx_command_history_user_host" ON "command_history" ("user_id", "host_id"); +ALTER TABLE "command_history" RENAME TO "p_ssh_terminal_command_history"; diff --git a/plugins/ssh-terminal/migrations/snapshot.json b/plugins/ssh-terminal/migrations/snapshot.json new file mode 100644 index 000000000..3aa6f08f1 --- /dev/null +++ b/plugins/ssh-terminal/migrations/snapshot.json @@ -0,0 +1,39 @@ +{ + "version": 1, + "tables": { + "command_history": { + "columns": { + "id": { + "type": "id", + "primaryKey": true, + "notNull": true + }, + "userId": { + "type": "refUser", + "length": 255, + "notNull": true + }, + "hostId": { + "type": "refHost", + "notNull": true + }, + "command": { + "type": "text", + "notNull": true + }, + "executedAt": { + "type": "timestamp", + "notNull": true, + "defaultNow": true + } + }, + "indexes": [ + { + "name": "idx_command_history_user_host", + "columns": ["userId", "hostId"] + } + ], + "adopts": "command_history" + } + } +} diff --git a/plugins/ssh-terminal/migrations/sqlite/0001_adopt_command_history.sql b/plugins/ssh-terminal/migrations/sqlite/0001_adopt_command_history.sql new file mode 100644 index 000000000..6d987fe65 --- /dev/null +++ b/plugins/ssh-terminal/migrations/sqlite/0001_adopt_command_history.sql @@ -0,0 +1,14 @@ +-- ssh-terminal 0001: adopt_command_history +-- Generated by termix-plugin migrations. Review before committing. + +CREATE TABLE IF NOT EXISTS "command_history" ( + "id" integer PRIMARY KEY AUTOINCREMENT, + "user_id" text NOT NULL, + "host_id" integer NOT NULL, + "command" text NOT NULL, + "executed_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE, + FOREIGN KEY ("host_id") REFERENCES "ssh_data" ("id") ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS "idx_command_history_user_host" ON "command_history" ("user_id", "host_id"); +ALTER TABLE "command_history" RENAME TO "p_ssh_terminal_command_history"; diff --git a/plugins/ssh-terminal/src/backend/helpers.ts b/plugins/ssh-terminal/src/backend/helpers.ts new file mode 100644 index 000000000..33ec941ef --- /dev/null +++ b/plugins/ssh-terminal/src/backend/helpers.ts @@ -0,0 +1,284 @@ +import dns from "dns/promises"; +import net from "net"; +import type { RawData } from "ws"; +import ssh2Pkg, { + type GetStreamCallback, + type IdentityCallback, + type ParsedKey, + type SignCallback, + type SigningRequestOptions, +} from "ssh2"; +import type { PluginLogger } from "@termix/plugin-sdk/backend"; + +const { AgentProtocol, BaseAgent } = ssh2Pkg; + +export function getErrorMessage( + error: unknown, + fallback = "Unknown error", +): string { + return error instanceof Error ? error.message : fallback; +} + +/** The terminal's structured logging, on top of ctx.log. */ +export interface TerminalLogger { + info: (message: string, meta?: Record<string, unknown>) => void; + success: (message: string, meta?: Record<string, unknown>) => void; + warn: (message: string, meta?: Record<string, unknown>) => void; + error: ( + message: string, + error?: unknown, + meta?: Record<string, unknown>, + ) => void; +} + +function withMeta(message: string, meta?: Record<string, unknown>): string { + if (!meta || Object.keys(meta).length === 0) return message; + try { + return `${message} ${JSON.stringify(meta)}`; + } catch { + return message; + } +} + +export function createTerminalLogger(log: PluginLogger): TerminalLogger { + return { + info: (message, meta) => log.info(withMeta(message, meta)), + success: (message, meta) => log.info(withMeta(message, meta)), + warn: (message, meta) => log.warn(withMeta(message, meta)), + error: (message, error, meta) => { + // Some call sites pass the meta object where the error goes. + if (error && !(error instanceof Error) && typeof error === "object") { + log.error(withMeta(message, error as Record<string, unknown>)); + return; + } + log.error( + withMeta(message, meta), + error instanceof Error + ? error + : error + ? new Error(String(error)) + : undefined, + ); + }, + }; +} + +// Cap on a single decoded text frame. Legitimate control messages are tiny, +// and terminal input is bounded by what a user can type or paste. +export const MAX_WS_MESSAGE_BYTES = 1024 * 1024; + +export class WsMessageError extends Error {} + +function rawByteLength(raw: RawData): number { + if (Buffer.isBuffer(raw)) return raw.length; + if (Array.isArray(raw)) + return raw.reduce((sum, part) => sum + part.length, 0); + if (raw instanceof ArrayBuffer) return raw.byteLength; + return 0; +} + +/** Parses a frame into `{ type, data }`, throwing WsMessageError on anything else. */ +export function parseWsMessage(raw: RawData): { + type: string; + data: unknown; +} { + if (rawByteLength(raw) > MAX_WS_MESSAGE_BYTES) { + throw new WsMessageError("Message too large"); + } + + let parsed: unknown; + try { + parsed = JSON.parse(raw.toString()); + } catch { + throw new WsMessageError("Invalid JSON"); + } + + if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) { + throw new WsMessageError("Message must be a JSON object"); + } + + const { type, data } = parsed as { type?: unknown; data?: unknown }; + if (typeof type !== "string") { + throw new WsMessageError("Message type must be a string"); + } + + return { type, data }; +} + +export function asObject(data: unknown): Record<string, unknown> { + return data !== null && typeof data === "object" && !Array.isArray(data) + ? (data as Record<string, unknown>) + : {}; +} + +export function asString(value: unknown): string { + return typeof value === "string" ? value : ""; +} + +/** A sane integer width or height, or 0 when the value is unusable. */ +export function toTerminalDimension(value: unknown): number { + const n = typeof value === "number" ? value : Number(value); + if (!Number.isFinite(n)) return 0; + const rounded = Math.floor(n); + if (rounded < 1) return 0; + return Math.min(rounded, 10000); +} + +export function isWindowsSftpPath(path: string): boolean { + const normalized = path.replace(/\\/g, "/"); + return /^[A-Za-z]:\//.test(normalized) || /^\/[A-Za-z]:\//.test(normalized); +} + +export function sftpPathToLocalPath(sftpPath: string): string { + const normalized = sftpPath.replace(/\\/g, "/"); + if (/^\/[A-Za-z]:\//.test(normalized)) { + return normalized.slice(1).replace(/\//g, "\\"); + } + if (/^[A-Za-z]:\//.test(normalized)) { + return normalized.replace(/\//g, "\\"); + } + return sftpPath; +} + +export const SSH_DNS_RETRY_DELAYS_MS = [250, 750, 1500]; + +type Lookup = typeof dns.lookup; +type Sleep = (ms: number) => Promise<void>; +const sleep: Sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +export function isRetriableDnsError(error: unknown): boolean { + const err = error as { code?: unknown; message?: unknown }; + return ( + err.code === "EAI_AGAIN" || + (typeof err.message === "string" && err.message.includes("EAI_AGAIN")) + ); +} + +/** Resolves a hostname up front, retrying transient container DNS failures. */ +export async function resolveHostForSshConnect( + host: string, + lookup: Lookup = dns.lookup, + retryDelaysMs = SSH_DNS_RETRY_DELAYS_MS, + wait: Sleep = sleep, +): Promise<{ host: string; resolvedAddress?: string; attempts: number }> { + const normalized = host.replace(/^\[|\]$/g, "").trim(); + if (!normalized || net.isIP(normalized) !== 0) { + return { host: normalized || host, attempts: 0 }; + } + + for (let attempt = 0; ; attempt += 1) { + try { + const result = await lookup(normalized); + return { + host: result.address, + resolvedAddress: result.address, + attempts: attempt + 1, + }; + } catch (error) { + if (!isRetriableDnsError(error) || attempt >= retryDelaysMs.length) { + throw error; + } + await wait(retryDelaysMs[attempt]); + } + } +} + +function normalizeHostAddress(value: unknown): string { + if (typeof value !== "string") return ""; + return value + .replace(/^\[|\]$/g, "") + .trim() + .toLowerCase(); +} + +/** + * Whether a numeric host id resolved to a different machine than the client + * meant. Ids from the desktop app and a sync server drift apart, and the + * resolved row supplies the credentials, so a mismatch must be refused. + */ +export function hostAddressMismatch( + clientAddress: unknown, + resolvedAddress: unknown, +): boolean { + const resolved = normalizeHostAddress(resolvedAddress); + if (!resolved) return false; + return resolved !== normalizeHostAddress(clientAddress); +} + +export const HOST_ADDRESS_MISMATCH_MESSAGE = + "Host mismatch: this server resolved the selected host to a different machine, so the connection was refused. " + + "The host ids on this device and on the sync server have drifted apart. " + + 'Set the connection origin to "This device" for this host, or re-run a full sync, then try again.'; + +export const HOST_NOT_ON_THIS_SERVER_MESSAGE = + "This host does not exist on the sync server, so the connection was refused. " + + 'Run a sync so the server knows about it, or set the connection origin to "This device" for this host.'; + +export function resolveServerJumpHosts( + clientJumpHosts: Array<{ hostId: number }> | undefined, + serverJumpHosts: Array<{ hostId: number }> | undefined, + hostSyncId?: string | null, +): Array<{ hostId: number }> | undefined { + if (hostSyncId) return serverJumpHosts ?? []; + return clientJumpHosts?.length ? clientJumpHosts : serverJumpHosts; +} + +export function resolveServerHostId( + clientHostId: number | null, + resolvedHost: { id?: unknown } | null, +): number | null { + return typeof resolvedHost?.id === "number" ? resolvedHost.id : clientHostId; +} + +/** An in-memory SSH agent holding one key, for agent forwarding. */ +export class MemoryAgent extends BaseAgent { + private key: ParsedKey; + + constructor(key: ParsedKey) { + super(); + this.key = key; + } + + getIdentities(cb: IdentityCallback<ParsedKey>): void { + cb(null, [this.key]); + } + + getStream(cb: GetStreamCallback): void { + const protocol = new AgentProtocol(false); + + protocol.on("identities", (request) => { + protocol.getIdentitiesReply(request, [this.key]); + }); + + protocol.on("sign", (request, publicKey, data, options) => { + this.sign(publicKey, data, options, (error, signature) => { + if (error || !signature) return protocol.failureReply(request); + protocol.signReply(request, signature); + }); + }); + + cb(null, protocol); + } + + sign( + _pubKey: ParsedKey | Buffer | string, + data: Buffer, + optionsOrCb: SigningRequestOptions | SignCallback, + cb?: SignCallback, + ): void { + const callback = typeof optionsOrCb === "function" ? optionsOrCb : cb!; + const options = typeof optionsOrCb === "function" ? {} : optionsOrCb; + try { + const algo = + options.hash === "sha256" + ? "rsa-sha2-256" + : options.hash === "sha512" + ? "rsa-sha2-512" + : undefined; + const signature = this.key.sign(data, algo); + callback(null, signature); + } catch (err) { + callback(err instanceof Error ? err : new Error(String(err))); + } + } +} diff --git a/plugins/ssh-terminal/src/backend/history-repository.ts b/plugins/ssh-terminal/src/backend/history-repository.ts new file mode 100644 index 000000000..f6894b00d --- /dev/null +++ b/plugins/ssh-terminal/src/backend/history-repository.ts @@ -0,0 +1,91 @@ +import { and, desc, eq, sql } from "drizzle-orm"; +import type { PluginDatabase } from "@termix/plugin-sdk/backend"; + +/* eslint-disable @typescript-eslint/no-explicit-any */ +// The table comes from ctx.db.define, which the SDK hands back untyped, and +// the drizzle handle is the server's own. Typed at this module's edge instead. +type Table = any; +type Drizzle = any; +/* eslint-enable @typescript-eslint/no-explicit-any */ + +export type HistoryRepository = ReturnType<typeof createHistoryRepository>; + +/** Command history storage. Writes are followed by persist(), reads are not. */ +export function createHistoryRepository(db: PluginDatabase, table: Table) { + const client = () => db.client<Drizzle>(); + + return { + async create( + userId: string, + hostId: number, + command: string, + executedAt = new Date().toISOString(), + ): Promise<void> { + const drizzle = await client(); + await drizzle + .insert(table) + .values({ userId, hostId, command, executedAt }); + await db.persist(); + }, + + async listUniqueCommandsForHost( + userId: string, + hostId: number, + limit = 500, + ): Promise<string[]> { + const drizzle = await client(); + const rows = await drizzle + .select({ + command: table.command, + maxExecutedAt: sql<number>`MAX(${table.executedAt})`, + }) + .from(table) + .where(and(eq(table.userId, userId), eq(table.hostId, hostId))) + .groupBy(table.command) + .orderBy(desc(sql`MAX(${table.executedAt})`)) + .limit(limit); + return rows.map((row: { command: string }) => row.command); + }, + + async listCommandsForHost( + userId: string, + hostId: number, + limit = 200, + ): Promise<Array<{ command: string; executedAt: string }>> { + const drizzle = await client(); + const rows = await drizzle + .select({ command: table.command, executedAt: table.executedAt }) + .from(table) + .where(and(eq(table.userId, userId), eq(table.hostId, hostId))) + .orderBy(desc(table.executedAt)) + .limit(limit); + return rows as Array<{ command: string; executedAt: string }>; + }, + + async deleteCommandForHost( + userId: string, + hostId: number, + command: string, + ): Promise<void> { + const drizzle = await client(); + await drizzle + .delete(table) + .where( + and( + eq(table.userId, userId), + eq(table.hostId, hostId), + eq(table.command, command), + ), + ); + await db.persist(); + }, + + async deleteByUserAndHost(userId: string, hostId: number): Promise<void> { + const drizzle = await client(); + await drizzle + .delete(table) + .where(and(eq(table.userId, userId), eq(table.hostId, hostId))); + await db.persist(); + }, + }; +} diff --git a/plugins/ssh-terminal/src/backend/host-import.ts b/plugins/ssh-terminal/src/backend/host-import.ts new file mode 100644 index 000000000..d83b3d3ef --- /dev/null +++ b/plugins/ssh-terminal/src/backend/host-import.ts @@ -0,0 +1,20 @@ +/** + * Registered as ctx.registry.provide("ssh-terminal.hostImportNormalizer", ...) + * so a Termix-JSON host import keeps its terminal switches, which used to be + * host columns and are this plugin's host settings now. + */ +export function hostImportNormalizer( + raw: Record<string, unknown>, +): Record<string, unknown> | null { + const keys = [ + "enableTerminal", + "enableTerminalToolbar", + "enableCommandHistory", + ] as const; + if (keys.every((key) => raw[key] === undefined)) return null; + const values: Record<string, unknown> = {}; + for (const key of keys) { + if (raw[key] !== undefined) values[key] = raw[key] !== false; + } + return values; +} diff --git a/src/backend/database/routes/terminal-image-storage-settings.ts b/plugins/ssh-terminal/src/backend/images/image-storage-settings.ts similarity index 96% rename from src/backend/database/routes/terminal-image-storage-settings.ts rename to plugins/ssh-terminal/src/backend/images/image-storage-settings.ts index a6efc6a87..1ec3f3470 100644 --- a/src/backend/database/routes/terminal-image-storage-settings.ts +++ b/plugins/ssh-terminal/src/backend/images/image-storage-settings.ts @@ -1,5 +1,4 @@ import path from "path"; -import { databaseLogger } from "../../utils/logger.js"; /** * Terminal image storage modes. @@ -120,16 +119,10 @@ function parseClampedInt(value: unknown, min: number): number | null { return Math.max(min, Math.trunc(parsed)); } -interface SettingSource { +export interface SettingSource { get(key: string): Promise<string | null>; -} - -function warnInvalid(key: string, source: string): void { - databaseLogger.warn("Ignoring invalid terminal image storage setting", { - operation: "terminal_image_storage_settings_invalid", - key, - source, - }); + /** Told about a stored or environment value that was ignored. */ + warn?: (message: string) => void; } function hasUnsafePathSyntax(value: string): boolean { @@ -157,6 +150,11 @@ export async function resolveTerminalImageStorageSettings( settings: SettingSource, env: NodeJS.ProcessEnv = process.env, ): Promise<TerminalImageStorageSettings> { + const warnInvalid = (key: string, source: string) => + settings.warn?.( + `Ignoring invalid terminal image storage setting ${key} from ${source}`, + ); + async function pick<T>( key: string, envName: string, diff --git a/src/backend/database/routes/terminal-image-storage.ts b/plugins/ssh-terminal/src/backend/images/image-storage.ts similarity index 99% rename from src/backend/database/routes/terminal-image-storage.ts rename to plugins/ssh-terminal/src/backend/images/image-storage.ts index 7022a9ee8..3d4b6d253 100644 --- a/src/backend/database/routes/terminal-image-storage.ts +++ b/plugins/ssh-terminal/src/backend/images/image-storage.ts @@ -5,8 +5,8 @@ import { exceedsImageStorageLimit, isExpiredImage, isImageFilename, -} from "./terminal-image-utils.js"; -import type { TerminalImageStorageSettings } from "./terminal-image-storage-settings.js"; +} from "./image-utils.js"; +import type { TerminalImageStorageSettings } from "./image-storage-settings.js"; /** * Stable error codes for image storage failures. These are part of the upload diff --git a/src/backend/database/routes/terminal-image-utils.ts b/plugins/ssh-terminal/src/backend/images/image-utils.ts similarity index 100% rename from src/backend/database/routes/terminal-image-utils.ts rename to plugins/ssh-terminal/src/backend/images/image-utils.ts diff --git a/plugins/ssh-terminal/src/backend/index.ts b/plugins/ssh-terminal/src/backend/index.ts index a25d3af44..7ce0b9def 100644 --- a/plugins/ssh-terminal/src/backend/index.ts +++ b/plugins/ssh-terminal/src/backend/index.ts @@ -1,32 +1,177 @@ +import type { Router } from "express"; import type { PluginContext } from "@termix/plugin-sdk/backend"; +import { createTerminalLogger } from "./helpers.js"; +import { hostImportNormalizer } from "./host-import.js"; +import { createHistoryRepository } from "./history-repository.js"; +import { registerTerminalRoutes } from "./routes.js"; import { - handleTerminalUpgrade, - sessionManager, - stopTerminalServer, -} from "../../../../src/backend/hosts/terminal/index.js"; + DEFAULT_TIMEOUT_MINUTES, + TerminalSessionManager, + type TerminalSession, +} from "./session-manager.js"; +import { + SESSION_GUESTS_KEY, + type LiveSessionInfo, + type LiveSessionsV1, + type RecordingsWriterV1, + type SessionGuestsV1, + type SessionSharingV1, + type TerminalHistoryV1, + type TmuxSessionsV1, +} from "./services.js"; +import { ADMIN_KEYS } from "./settings.js"; +import { commandHistory } from "./tables.js"; +import { createTerminalSocket } from "./terminal-socket.js"; + +function toInfo(session: TerminalSession): LiveSessionInfo { + return { + id: session.id, + userId: session.userId, + hostId: session.hostId, + hostName: session.hostName, + isConnected: session.isConnected, + createdAt: session.createdAt, + lastDetachedAt: session.lastDetachedAt, + tabInstanceId: + session.attachedTabInstanceId ?? session.tabInstanceId ?? null, + tmuxSessionName: session.tmuxSessionName, + cols: session.cols, + rows: session.rows, + }; +} + +/** + * A service another plugin may or may not provide. The handle always exists; + * whether its provider is up is asked through the proxy's `in` check. + */ +function optionalService<T extends object>( + ctx: PluginContext, + service: string, + probe: keyof T & string, +): T | null { + const handle = ctx.services.get<T>(service); + return probe in handle ? handle : null; +} export async function activate(ctx: PluginContext) { - // Public because the terminal authenticates each socket itself: a share-link - // guest arrives with a share token instead of a session, so the check has to - // happen where that token is understood. - ctx.ws.upgrade( - "/terminal", - (request, socket, head) => - handleTerminalUpgrade( - request as Parameters<typeof handleTerminalUpgrade>[0], - socket as Parameters<typeof handleTerminalUpgrade>[1], - head as Buffer, - ), - { public: true }, - ); + const log = createTerminalLogger(ctx.log); - // Published so collab and session-sharing can reach live sessions without - // importing a plugin. Revoked automatically when this plugin is disabled. - ctx.registry.provide("terminal.sessions", sessionManager ?? null); + const table = await ctx.db.define(commandHistory); + const history = createHistoryRepository(ctx.db, table); + + // Read on every detach, so it is kept current rather than awaited there. + let timeoutMinutes = DEFAULT_TIMEOUT_MINUTES; + const applyTimeout = (value: unknown) => { + const minutes = Number(value); + timeoutMinutes = + Number.isFinite(minutes) && minutes > 0 + ? minutes + : DEFAULT_TIMEOUT_MINUTES; + }; + const refreshTimeout = async () => + applyTimeout(await ctx.settings.get(ADMIN_KEYS.sessionTimeoutMinutes)); + await refreshTimeout(); + ctx.settings.onChange(ADMIN_KEYS.sessionTimeoutMinutes, applyTimeout); + + const sessionManager = new TerminalSessionManager({ + log, + getTimeoutMinutes: () => timeoutMinutes, + getRecordings: () => + optionalService<RecordingsWriterV1>(ctx, "recordings.writer", "open"), + }); + ctx.disposables.add(() => sessionManager.destroyAll()); + + const socket = createTerminalSocket({ + ctx, + log, + sessionManager, + getTmux: () => + optionalService<TmuxSessionsV1>(ctx, "tmux.sessions", "detect"), + getSharing: () => + optionalService<SessionSharingV1>( + ctx, + "sessions.sharing", + "authorizeJoin", + ), + getGuests: () => + ctx.registry.consume<SessionGuestsV1>(SESSION_GUESTS_KEY) ?? null, + }); + + // Public with optional auth: a share-link guest arrives with a share token + // instead of a session, so that check happens where the token is understood. + ctx.ws.route( + "/terminal", + (connection) => { + // Also picks up a value the boot migration wrote after activation. + void refreshTimeout().catch(() => {}); + return socket.handleConnection(connection); + }, + { + public: true, + optionalAuth: true, + }, + ); + ctx.disposables.add(() => socket.closeAll()); + + registerTerminalRoutes(ctx.http.router<Router>({ rawBody: true }), { + ctx, + log, + sessionManager, + history, + }); + + const liveSessions: LiveSessionsV1 = { + getSession: (sessionId) => { + const session = sessionManager.getSession(sessionId); + return session ? toInfo(session) : null; + }, + listForUser: (userId) => { + // A plugin caller only ever sees its actor's sessions; core calls this + // outside any actor, for the user its own route authenticated. + const actor = ctx.currentActor(); + if (actor && actor !== userId) return []; + return sessionManager.getUserSessions(userId).map(toInfo); + }, + ownerEndSession: (sessionId, reason) => + sessionManager.ownerEndSession(sessionId, reason), + disconnectParticipants: (sessionId, shareId, options) => + sessionManager.disconnectShareParticipants(sessionId, shareId, options), + setRoomShareControl: (sessionId, shareId, controllerUserId) => + sessionManager.setRoomShareControl(sessionId, shareId, controllerUserId), + subscribe: (sessionId, onData) => { + const session = sessionManager.getSession(sessionId); + if (!session) return () => {}; + session.dataListeners.add(onData); + return () => session.dataListeners.delete(onData); + }, + write: (sessionId, data) => { + const session = sessionManager.getSession(sessionId); + if (!session?.sshStream || session.sshStream.destroyed) return false; + sessionManager.bufferInput(sessionId, data); + session.sshStream.write(data); + return true; + }, + idleTimeoutMinutes: () => timeoutMinutes, + }; + ctx.services.provide("sessions.live", liveSessions); + + const terminalHistory: TerminalHistoryV1 = { + list: async (hostId, limit = 200) => { + const userId = ctx.currentActor(); + if (!userId) return []; + return history.listCommandsForHost(userId, hostId, limit); + }, + }; + ctx.services.provide("terminal.history", terminalHistory); + + ctx.registry.provide( + "ssh-terminal.hostImportNormalizer", + hostImportNormalizer, + ); ctx.log.info("SSH terminal mounted at /plugin-ws/ssh-terminal/terminal"); } export async function deactivate() { - await stopTerminalServer(); + // Everything above was registered through ctx and is disposed by core. } diff --git a/src/backend/hosts/connect/terminal-prompt.ts b/plugins/ssh-terminal/src/backend/keyboard-prompt.ts similarity index 80% rename from src/backend/hosts/connect/terminal-prompt.ts rename to plugins/ssh-terminal/src/backend/keyboard-prompt.ts index 5f2e5cf59..95f0054c2 100644 --- a/src/backend/hosts/connect/terminal-prompt.ts +++ b/plugins/ssh-terminal/src/backend/keyboard-prompt.ts @@ -1,19 +1,37 @@ /** * Keyboard-interactive answering over the terminal WebSocket. * - * The classifier decides what a round is; this keeps the terminal's message - * protocol (totp_required, totp_retry, password_required, - * warpgate_auth_required) and its timeouts exactly as the client expects. + * Core's classifier (ctx.ssh.classifyKeyboardInteractive) decides what a + * round is; this keeps the terminal's message protocol (totp_required, + * totp_retry, password_required, warpgate_auth_required) and its timeouts + * exactly as the client expects. */ import type { WebSocket } from "ws"; -import { sshLogger, authLogger } from "../../utils/logger.js"; -import { ensureCoreSshAuthProviders } from "./core-providers.js"; -import { - classifyKeyboardInteractive, - responsesWithAnswer, -} from "./keyboard-interactive.js"; -import type { KeyboardInteractivePrompt, SshConnectHost } from "./types.js"; +import type { + PluginKeyboardInteractivePrompt, + PluginSsh, + PluginSshHost, +} from "@termix/plugin-sdk/backend"; +import type { TerminalLogger } from "./helpers.js"; + +type KeyboardInteractivePrompt = PluginKeyboardInteractivePrompt; + +const PASSWORD_PATTERN = /password/i; + +/** The answer at one index, the stored password for password prompts, else empty. */ +export function responsesWithAnswer( + prompts: KeyboardInteractivePrompt[], + answerIndex: number, + answer: string, + password: string | null | undefined, +): string[] { + return prompts.map((p, index) => { + if (index === answerIndex) return answer; + if (PASSWORD_PATTERN.test(p.prompt) && password) return password; + return ""; + }); +} interface ResolvedCredentials { username: string; @@ -27,6 +45,8 @@ interface HostFlags { } interface AuthContext { + ssh: Pick<PluginSsh, "classifyKeyboardInteractive">; + log: TerminalLogger; userId: string; ws: WebSocket; hostId: number; @@ -60,7 +80,6 @@ export class SSHAuthManager { resolvedCredentials: ResolvedCredentials, hostConfig?: HostFlags, ): void { - ensureCoreSshAuthProviders(); this.context.isKeyboardInteractive = true; const host = { @@ -71,9 +90,9 @@ export class SSHAuthManager { username: resolvedCredentials.username, password: resolvedCredentials.password, authType: resolvedCredentials.authType, - } as SshConnectHost; + } as PluginSshHost; - const decision = classifyKeyboardInteractive( + const decision = this.context.ssh.classifyKeyboardInteractive( { name, instructions, prompts }, host, ); @@ -86,7 +105,12 @@ export class SSHAuthManager { this.handleWarpgate(decision, finish); return; case "totp": - this.handleTotp(prompts, decision.promptIndex, finish, host.password); + this.handleTotp( + prompts, + decision.promptIndex, + finish, + host.password as string | undefined, + ); return; case "input": this.handleInput( @@ -94,7 +118,7 @@ export class SSHAuthManager { decision.promptIndex, decision.isPush, finish, - host.password, + host.password as string | undefined, ); return; } @@ -122,7 +146,7 @@ export class SSHAuthManager { if (this.context.keyboardInteractiveFinish) { this.context.keyboardInteractiveFinish = null; this.context.warpgateAuthPromptSent = false; - sshLogger.warn("Warpgate authentication timeout", { + this.context.log.warn("Warpgate authentication timeout", { operation: "warpgate_timeout", hostId: this.context.hostId, }); @@ -143,11 +167,11 @@ export class SSHAuthManager { password: string | undefined, ): void { if (this.context.totpPromptSent) { - sshLogger.warn("TOTP prompt asked again - invalid code", { + this.context.log.warn("TOTP prompt asked again - invalid code", { operation: "ssh_keyboard_interactive_totp_retry", hostId: this.context.hostId, }); - authLogger.warn("TOTP verification failed for SSH session", { + this.context.log.warn("TOTP verification failed for SSH session", { operation: "terminal_totp_failed", userId: this.context.userId, hostId: this.context.hostId, @@ -172,7 +196,7 @@ export class SSHAuthManager { this.armTimeout(TOTP_TIMEOUT_MS, "totp"); this.sendLog("auth", "info", "TOTP verification required"); - authLogger.info("TOTP verification prompt sent to client", { + this.context.log.info("TOTP verification prompt sent to client", { operation: "terminal_totp_prompt", userId: this.context.userId, hostId: this.context.hostId, @@ -227,7 +251,7 @@ export class SSHAuthManager { } else { this.context.keyboardInteractiveResponded = false; } - sshLogger.warn( + this.context.log.warn( `${kind === "totp" ? "TOTP" : "Password"} prompt timeout`, { operation: `${kind}_timeout`, diff --git a/plugins/ssh-terminal/src/backend/routes.ts b/plugins/ssh-terminal/src/backend/routes.ts new file mode 100644 index 000000000..effe04d7a --- /dev/null +++ b/plugins/ssh-terminal/src/backend/routes.ts @@ -0,0 +1,801 @@ +import express, { + type NextFunction, + type Request, + type Response, + type Router, +} from "express"; +import { randomUUID } from "crypto"; +import multer from "multer"; +import type { PluginContext } from "@termix/plugin-sdk/backend"; +import { getErrorMessage, type TerminalLogger } from "./helpers.js"; +import type { HistoryRepository } from "./history-repository.js"; +import type { TerminalSessionManager } from "./session-manager.js"; +import { + ADMIN_KEYS, + HOST_KEYS, + readClientSettings, + readImageStorageSettings, +} from "./settings.js"; +import { + createConcurrencyLimiter, + exceedsNormalizedImageSize, + imageExtensionForFormat, +} from "./images/image-utils.js"; +import { + selectImageStorageMode, + probeLocalImageVisibility, + storeImageLocally, + storeImageViaSftp, + TerminalImageStorageError, + type ImageSftpClient, +} from "./images/image-storage.js"; + +function isNonEmptyString(val: unknown): val is string { + return typeof val === "string" && val.trim().length > 0; +} + +export const SENSITIVE_COMMAND_PATTERNS = [ + /passw(or)?d/i, + /\bsecret\b/i, + /\btoken\b/i, + /\bapi.?key\b/i, + /PASS(WORD)?=/i, + /AWS_SECRET/i, + /mysql\b.*-p/i, + /sudo\s+-S\b/, + /htpasswd/i, + /sshpass/i, + /curl\b.*-u\s/i, + /export\b.*(?:PASSWORD|SECRET|TOKEN|KEY)=/i, +]; + +type SharpFactory = typeof import("sharp").default; + +export interface TerminalRouteDeps { + ctx: PluginContext; + log: TerminalLogger; + sessionManager: TerminalSessionManager; + history: HistoryRepository; +} + +/** + * The terminal's HTTP routes, on a router core mounts at + * /plugin-api/ssh-terminal/. It is created with rawBody so multer can read + * the upload stream; JSON routes parse their own body. + */ +export function registerTerminalRoutes( + router: Router, + { ctx, log, sessionManager, history }: TerminalRouteDeps, +): void { + const json = express.json({ limit: "2mb" }); + + // Browser image handoff for local terminal-agent workflows. + const imageUpload = multer({ + storage: multer.memoryStorage(), + limits: { + fileSize: 50 * 1024 * 1024, + fields: 4, + fieldSize: 64 * 1024, + files: 1, + parts: 5, + headerPairs: 200, + }, + }); + const imageUploadMiddleware = imageUpload.single("image"); + + let sharpFactory: SharpFactory | null = null; + let sharpLoadFailure: string | null = null; + + // A static import of sharp takes the plugin down with it when the native + // binary for the running architecture is missing. Resolve on first use so + // only image upload is lost. + async function loadSharp(): Promise<SharpFactory> { + if (sharpFactory) return sharpFactory; + if (sharpLoadFailure !== null) throw new Error(sharpLoadFailure); + try { + sharpFactory = (await import("sharp")).default; + return sharpFactory; + } catch (error) { + sharpLoadFailure = getErrorMessage(error, "unknown"); + throw new Error(sharpLoadFailure); + } + } + + const imageProcessingLimiter = createConcurrencyLimiter(4, 4); + const imageMultipartAdmissionLimiter = createConcurrencyLimiter(4, 4); + let imageUploadSequence = 0; + + async function handleImageUploadMiddleware( + req: Request, + res: Response, + next: NextFunction, + ): Promise<void> { + let releaseAdmission: (() => void) | undefined; + try { + releaseAdmission = await imageMultipartAdmissionLimiter.acquire(); + } catch { + res.status(503).json({ + error: "Image upload capacity is temporarily unavailable", + code: "IMAGE_UPLOAD_CAPACITY_EXCEEDED", + }); + return; + } + + imageUploadMiddleware(req, res, (error: unknown) => { + try { + if (!error) { + next(); + return; + } + if (error instanceof multer.MulterError) { + log.warn("Image upload multipart request rejected", { + operation: "terminal_image_upload_multipart_rejected", + code: error.code, + field: error.field, + contentType: req.headers["content-type"]?.split(";", 1)[0], + }); + res.status(400).json({ + error: "Image upload request rejected", + code: error.code, + field: error.field, + }); + return; + } + log.warn("Image upload multipart request malformed", { + operation: "terminal_image_upload_multipart_invalid", + contentType: req.headers["content-type"]?.split(";", 1)[0], + }); + res.status(400).json({ + error: "Malformed image upload request", + code: "IMAGE_MULTIPART_INVALID", + }); + } finally { + releaseAdmission?.(); + } + }); + } + + function findTerminalSession(userId: string, instanceId: string) { + return sessionManager + .getUserSessions(userId) + .find( + (session) => + (session.attachedTabInstanceId ?? session.tabInstanceId) === + instanceId && session.isConnected, + ); + } + + /** + * @openapi + * /plugin-api/ssh-terminal/image-upload: + * post: + * summary: Upload an image into a terminal session + * description: Normalizes an uploaded or pasted image to PNG and stores it where the terminal's programs can read it, over SFTP on the connected host or in the backend's mapped local storage, depending on the image storage settings. Returns the path to hand to the program. + * tags: + * - Terminal + * requestBody: + * required: true + * content: + * multipart/form-data: + * schema: + * type: object + * properties: + * image: + * type: string + * format: binary + * instanceId: + * type: string + * source: + * type: string + * enum: [file, clipboard] + * clientUploadTimestamp: + * type: string + * responses: + * 200: + * description: The stored image's path. + * 400: + * description: Missing or invalid image or session. + * 409: + * description: The terminal is not connected. + * 413: + * description: The normalized image is too large. + * 502: + * description: Writing to the remote host failed. + * 503: + * description: Image storage or processing is unavailable. + * 507: + * description: The image storage limit was reached. + */ + router.post( + "/image-upload", + handleImageUploadMiddleware, + async (req: Request, res: Response) => { + const userId = ctx.currentActor(); + const instanceId = req.body?.instanceId; + if (!req.file) { + return res.status(400).json({ + error: "Image required", + code: "IMAGE_FILE_MISSING", + }); + } + if (!isNonEmptyString(userId)) { + return res.status(400).json({ + error: "Missing terminal session", + code: "IMAGE_SESSION_MISSING", + }); + } + + const requestId = randomUUID(); + const sequence = ++imageUploadSequence; + const source = + req.body?.source === "file" || req.body?.source === "clipboard" + ? req.body.source + : undefined; + const clientUploadTimestamp = + typeof req.body?.clientUploadTimestamp === "string" && + !Number.isNaN(Date.parse(req.body.clientUploadTimestamp)) + ? req.body.clientUploadTimestamp + : undefined; + const serverReceivedAt = new Date().toISOString(); + log.info("Terminal image upload received", { + operation: "terminal_image_upload_received", + requestId, + sequence, + source, + clientUploadTimestamp, + serverReceivedAt, + bytes: req.file.size, + }); + + const storageSettings = await readImageStorageSettings(ctx); + const session = isNonEmptyString(instanceId) + ? findTerminalSession(userId, instanceId) + : undefined; + let localHostVisible = false; + if (storageSettings.localMappingConfigured && session?.sshConn) { + localHostVisible = await probeLocalImageVisibility( + session.sshConn, + storageSettings, + ).catch(() => false); + } + const storageMode = selectImageStorageMode(storageSettings, { + remoteSftpAvailable: !!session?.sshConn, + localHostVisible, + }); + + if (storageMode === "unavailable") { + return res.status(503).json({ + error: "Image storage is unavailable", + code: "IMAGE_STORAGE_UNAVAILABLE", + }); + } + + if (storageMode === "local" && !storageSettings.localMappingConfigured) { + return res.status(503).json({ + error: "Local image storage is not configured", + code: "IMAGE_LOCAL_STORAGE_NOT_CONFIGURED", + }); + } + + if (storageMode === "remote-sftp") { + if (!isNonEmptyString(instanceId)) { + return res.status(400).json({ + error: "Missing terminal session", + code: "IMAGE_SESSION_MISSING", + }); + } + if (!session || !session.sshConn) { + return res.status(409).json({ + error: "Terminal is not connected", + code: "IMAGE_TERMINAL_NOT_CONNECTED", + }); + } + } + + let sharp: SharpFactory; + try { + sharp = await loadSharp(); + } catch (error) { + log.error("Image processing unavailable: sharp failed to load", error, { + operation: "terminal_image_upload_sharp_unavailable", + reason: getErrorMessage(error, "unknown"), + }); + return res.status(503).json({ + error: "Image processing is unavailable on this installation", + code: "IMAGE_PROCESSING_UNAVAILABLE", + }); + } + + let normalizedImage: Buffer; + let releaseImageProcessingSlot: (() => void) | undefined; + try { + releaseImageProcessingSlot = await imageProcessingLimiter.acquire(); + } catch { + return res.status(503).json({ + error: "Image upload capacity is temporarily exhausted", + code: "IMAGE_UPLOAD_CAPACITY_EXCEEDED", + }); + } + try { + const image = sharp(req.file.buffer, { + failOn: "error", + limitInputPixels: 40_000_000, + }); + const { format } = await image.metadata(); + if (!imageExtensionForFormat(format)) { + return res.status(400).json({ + error: "Unsupported image format", + code: "IMAGE_FORMAT_UNSUPPORTED", + }); + } + normalizedImage = await image.rotate().png().toBuffer(); + if (exceedsNormalizedImageSize(normalizedImage.length)) { + return res.status(413).json({ + error: "Normalized image is too large", + code: "IMAGE_NORMALIZED_SIZE_LIMIT", + }); + } + } catch (error) { + log.warn("Image upload failed image decoding", { + operation: "terminal_image_upload_decode", + mimeType: req.file.mimetype, + bytes: req.file.size, + reason: getErrorMessage(error, "unknown"), + }); + return res.status(400).json({ + error: "Invalid image data", + code: "IMAGE_DECODE_FAILED", + }); + } finally { + releaseImageProcessingSlot?.(); + } + + let remoteSftp: ImageSftpClient | undefined; + try { + const stored = + storageMode === "remote-sftp" + ? await (async () => { + remoteSftp = await new Promise<ImageSftpClient>( + (resolve, reject) => { + let settled = false; + const timer = setTimeout(() => { + settled = true; + reject(new Error("SFTP channel acquisition timed out")); + }, 3_000); + session!.sshConn!.sftp((err, sftp) => { + if (settled) { + sftp?.end?.(); + return; + } + settled = true; + clearTimeout(timer); + if (err) return reject(err); + resolve(sftp); + }); + }, + ); + return storeImageViaSftp(remoteSftp, normalizedImage, { + ttlMs: storageSettings.ttlMs, + maxCount: storageSettings.maxCount, + maxBytes: storageSettings.maxBytes, + }); + })() + : await storeImageLocally(normalizedImage, storageSettings); + + res.json(stored); + } catch (error) { + if (error instanceof TerminalImageStorageError) { + const status = + error.code === "IMAGE_STORAGE_LIMIT_REACHED" + ? 507 + : error.code === "IMAGE_REMOTE_WRITE_FAILED" + ? 502 + : error.code === "IMAGE_REMOTE_QUOTA_UNAVAILABLE" + ? 503 + : error.code === "IMAGE_LOCAL_INSPECTION_FAILED" + ? 503 + : 500; + log.warn("Image upload storage write failed", { + operation: + error.code === "IMAGE_REMOTE_WRITE_FAILED" + ? "terminal_image_upload_sftp_failed" + : "terminal_image_upload_local_failed", + code: error.code, + userId, + instanceId, + reason: getErrorMessage(error.cause ?? error, "unknown"), + }); + return res.status(status).json({ + error: error.message, + code: error.code, + }); + } + log.warn("Image upload failed to acquire remote channel", { + operation: "terminal_image_upload_sftp_failed", + code: "IMAGE_REMOTE_WRITE_FAILED", + userId, + instanceId, + reason: getErrorMessage(error, "unknown"), + }); + return res.status(502).json({ + error: "Failed to write image to the remote host", + code: "IMAGE_REMOTE_WRITE_FAILED", + }); + } finally { + remoteSftp?.end?.(); + } + }, + ); + + /** + * @openapi + * /plugin-api/ssh-terminal/image-storage/test: + * post: + * summary: Test image storage visibility (admin only) + * description: Reports which storage mode an upload would take for one of the caller's already-connected terminal sessions. Uses the bounded local-mapping probe only; it never opens new connections. + * tags: + * - Terminal + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * instanceId: + * type: string + * responses: + * 200: + * description: Visibility test result. + * 400: + * description: Missing terminal session instanceId. + * 403: + * description: Admin access required. + * 500: + * description: Failed to run the test. + */ + router.post("/image-storage/test", json, async (req, res) => { + if (!(await ctx.rbac.has("admin.plugins.manage"))) { + return res.status(403).json({ error: "Admin access required" }); + } + const userId = ctx.currentActor() ?? ""; + const instanceId = req.body?.instanceId; + if (typeof instanceId !== "string" || instanceId.trim().length === 0) { + return res.status(400).json({ + error: "Missing terminal session", + code: "IMAGE_SESSION_MISSING", + }); + } + + try { + const settings = await readImageStorageSettings(ctx); + const session = findTerminalSession(userId, instanceId); + const remoteSftpAvailable = !!session?.sshConn; + + let localHostVisible: boolean | null = null; + if (settings.localMappingConfigured && session?.sshConn) { + localHostVisible = await probeLocalImageVisibility( + session.sshConn, + settings, + ).catch(() => false); + } + + const selectedMode = selectImageStorageMode(settings, { + remoteSftpAvailable, + ...(localHostVisible !== null ? { localHostVisible } : {}), + }); + + res.json({ + mode: settings.mode, + connected: !!session, + remoteSftpAvailable, + localHostVisible, + selectedMode, + localMappingConfigured: settings.localMappingConfigured, + }); + } catch (err) { + log.error("Failed to test image storage visibility", err); + res + .status(500) + .json({ error: "Failed to test image storage visibility" }); + } + }); + + /** + * @openapi + * /plugin-api/ssh-terminal/client-settings: + * get: + * summary: Terminal settings every user's terminal needs + * description: The admin-set session timeout and persistence flag, whether command history is on, and the touch input tuning. Readable by any signed-in user; changing them goes through the plugin's admin settings. + * tags: + * - Terminal + * responses: + * 200: + * description: The settings. + * 500: + * description: Failed to load settings. + */ + router.get("/client-settings", async (_req, res) => { + try { + res.json(await readClientSettings(ctx)); + } catch (err) { + log.error("Failed to load terminal client settings", err); + res.status(500).json({ error: "Failed to load settings" }); + } + }); + + /** + * @openapi + * /plugin-api/ssh-terminal/command-history: + * post: + * summary: Save command to history + * description: Saves a command to the caller's history for a host. Commands that look like they carry a secret, and anything while command history is off globally or for the host, are acknowledged but not stored. + * tags: + * - Terminal + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * hostId: + * type: integer + * command: + * type: string + * responses: + * 201: + * description: Command saved, or skipped on purpose. + * 400: + * description: Missing required parameters. + * 500: + * description: Failed to save command. + */ + router.post("/command-history", json, async (req, res) => { + const userId = ctx.currentActor(); + const { hostId, command } = req.body ?? {}; + + if (!isNonEmptyString(userId) || !hostId || !isNonEmptyString(command)) { + log.warn("Invalid command history save request", { + operation: "command_history_save", + userId, + hasHostId: !!hostId, + hasCommand: !!command, + }); + return res.status(400).json({ error: "Missing required parameters" }); + } + + const hostIdNum = parseInt(hostId, 10); + const trimmedCommand = command.trim(); + const skipped = { + id: 0, + userId, + hostId: hostIdNum, + command: trimmedCommand, + executedAt: new Date().toISOString(), + }; + + if (SENSITIVE_COMMAND_PATTERNS.some((p) => p.test(trimmedCommand))) { + return res.status(201).json(skipped); + } + if ((await ctx.settings.get(ADMIN_KEYS.commandHistoryEnabled)) === false) { + return res.status(201).json(skipped); + } + if ( + (await ctx.settings.getHost( + hostIdNum, + HOST_KEYS.enableCommandHistory, + )) === false + ) { + return res.status(201).json(skipped); + } + + try { + const executedAt = new Date().toISOString(); + await history.create(userId, hostIdNum, trimmedCommand, executedAt); + res.status(201).json({ + userId, + hostId: hostIdNum, + command: trimmedCommand, + executedAt, + }); + } catch (err) { + log.error("Failed to save command to history", err); + res.status(500).json({ + error: getErrorMessage(err, "Failed to save command"), + }); + } + }); + + /** + * @openapi + * /plugin-api/ssh-terminal/command-history/{hostId}: + * get: + * summary: Get command history + * description: The caller's distinct commands on a host, most recently used first, for autocomplete. + * tags: + * - Terminal + * parameters: + * - in: path + * name: hostId + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: A list of commands. + * 400: + * description: Invalid request parameters. + * 500: + * description: Failed to fetch history. + */ + router.get("/command-history/:hostId", async (req, res) => { + const userId = ctx.currentActor(); + const hostIdNum = parseInt(String(req.params.hostId), 10); + + if (!isNonEmptyString(userId) || isNaN(hostIdNum)) { + log.warn("Invalid command history fetch request", { + userId, + hostId: hostIdNum, + }); + return res.status(400).json({ error: "Invalid request parameters" }); + } + + try { + res.json(await history.listUniqueCommandsForHost(userId, hostIdNum)); + } catch (err) { + log.error("Failed to fetch command history", err); + res.status(500).json({ + error: getErrorMessage(err, "Failed to fetch history"), + }); + } + }); + + /** + * @openapi + * /plugin-api/ssh-terminal/command-history/{hostId}/recent: + * get: + * summary: Get recent command history + * description: The caller's commands on a host, newest first, duplicates included. Needs hosts.view. + * tags: + * - Terminal + * parameters: + * - in: path + * name: hostId + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: A list of commands. + * 400: + * description: Invalid host id. + * 403: + * description: Missing the hosts.view permission. + * 500: + * description: Failed to fetch command history. + */ + router.get("/command-history/:hostId/recent", async (req, res) => { + if (!(await ctx.rbac.has("hosts.view"))) { + return res.status(403).json({ error: "Insufficient permissions" }); + } + const userId = ctx.currentActor(); + const hostIdNum = parseInt(String(req.params.hostId), 10); + if (!isNonEmptyString(userId) || !hostIdNum) { + return res.status(400).json({ error: "Invalid userId or hostId" }); + } + try { + const rows = await history.listCommandsForHost(userId, hostIdNum); + res.json(rows.map((row) => row.command)); + } catch (err) { + log.error("Failed to fetch command history from database", err, { + operation: "command_history_fetch", + hostId: hostIdNum, + }); + res.status(500).json({ error: "Failed to fetch command history" }); + } + }); + + /** + * @openapi + * /plugin-api/ssh-terminal/command-history/delete: + * post: + * summary: Delete a specific command from history + * description: Deletes one command from the caller's history for a host. + * tags: + * - Terminal + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * hostId: + * type: integer + * command: + * type: string + * responses: + * 200: + * description: Command deleted successfully. + * 400: + * description: Missing required parameters. + * 500: + * description: Failed to delete command. + */ + router.post("/command-history/delete", json, async (req, res) => { + const userId = ctx.currentActor(); + const { hostId, command } = req.body ?? {}; + + if (!isNonEmptyString(userId) || !hostId || !isNonEmptyString(command)) { + log.warn("Invalid command delete request", { + operation: "command_history_delete", + userId, + hasHostId: !!hostId, + hasCommand: !!command, + }); + return res.status(400).json({ error: "Missing required parameters" }); + } + + try { + await history.deleteCommandForHost( + userId, + parseInt(hostId, 10), + command.trim(), + ); + res.json({ success: true }); + } catch (err) { + log.error("Failed to delete command from history", err); + res.status(500).json({ + error: getErrorMessage(err, "Failed to delete command"), + }); + } + }); + + /** + * @openapi + * /plugin-api/ssh-terminal/command-history/{hostId}: + * delete: + * summary: Clear command history + * description: Clears the caller's whole command history for a host. + * tags: + * - Terminal + * parameters: + * - in: path + * name: hostId + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: Command history cleared successfully. + * 400: + * description: Invalid request. + * 500: + * description: Failed to clear history. + */ + router.delete("/command-history/:hostId", async (req, res) => { + const userId = ctx.currentActor(); + const hostIdNum = parseInt(String(req.params.hostId), 10); + + if (!isNonEmptyString(userId) || isNaN(hostIdNum)) { + log.warn("Invalid command history clear request"); + return res.status(400).json({ error: "Invalid request" }); + } + + try { + await history.deleteByUserAndHost(userId, hostIdNum); + log.info("Terminal history cleared", { + operation: "terminal_history_clear", + userId, + hostId: hostIdNum, + }); + res.json({ success: true }); + } catch (err) { + log.error("Failed to clear command history", err); + res.status(500).json({ + error: getErrorMessage(err, "Failed to clear history"), + }); + } + }); +} diff --git a/plugins/ssh-terminal/src/backend/services.ts b/plugins/ssh-terminal/src/backend/services.ts new file mode 100644 index 000000000..318feb051 --- /dev/null +++ b/plugins/ssh-terminal/src/backend/services.ts @@ -0,0 +1,143 @@ +/** + * The services this plugin provides and consumes. + * + * Provided: "sessions.live" (live SSH sessions, for session sharing, collab + * and recording) and "terminal.history" (command history, for the AI + * assistant). Consumed, all optional: "tmux.sessions" (B10), + * "sessions.sharing" (B12) and "recordings.writer" (B13). The terminal keeps + * working without any of them: no tmux attach, no shared joins, no recording. + */ + +export interface LiveSessionInfo { + id: string; + userId: string; + hostId: number; + hostName: string; + isConnected: boolean; + createdAt: number; + lastDetachedAt: number | null; + tabInstanceId: string | null; + tmuxSessionName: string | null; + cols: number; + rows: number; +} + +export interface LiveSessionsV1 { + getSession: (sessionId: string) => LiveSessionInfo | null; + /** The caller's own sessions. A plugin caller only ever gets its actor's. */ + listForUser: (userId: string) => LiveSessionInfo[]; + /** Tells every guest the owner ended it, then closes the session. */ + ownerEndSession: (sessionId: string, reason: string) => void; + /** Removes the participants that joined through one share. */ + disconnectParticipants: ( + sessionId: string, + shareId: string, + options: { reason: string; userId?: string | null }, + ) => number; + /** Hands input control of a room share to one member, or back to the owner. */ + setRoomShareControl: ( + sessionId: string, + shareId: string, + controllerUserId: string | null, + ) => void; + /** Output as it arrives. Returns the unsubscribe. */ + subscribe: (sessionId: string, onData: (data: string) => void) => () => void; + /** Types into the session. False when it is not live. */ + write: (sessionId: string, data: string) => boolean; + /** How long a detached session is kept, in minutes. */ + idleTimeoutMinutes: () => number; +} + +export interface CommandHistoryEntry { + command: string; + executedAt: string; +} + +export interface TerminalHistoryV1 { + /** The acting user's most recent commands on one host, newest first. */ + list: (hostId: number, limit?: number) => Promise<CommandHistoryEntry[]>; +} + +export interface TmuxDetection { + available: boolean; + sessions: string[]; +} + +/** + * What the terminal asks of the tmux plugin. Clients are ssh2 objects. Every + * method is async to a consumer: a service call goes through the permission + * check and the audit line first. + */ +export interface TmuxSessionsV1 { + detect: (client: unknown) => Promise<TmuxDetection>; + /** Writes the attach (or new-session) command into a shell stream. */ + attachOrCreate: ( + stream: unknown, + name?: string, + newName?: string, + ) => Promise<void>; + /** Waits for a new session to exist and returns its confirmed name. */ + waitForSession: (client: unknown, name: string) => Promise<string>; +} + +export interface SharedSessionRef { + id: string; + sessionId: string; + permissionLevel: "read-write" | "read-only"; +} + +/** For signed-in members; every call runs as the acting user. */ +export interface SessionSharingV1 { + /** A user share targeting the actor, or a room stage they are a member of. */ + authorizeJoin: ( + shareId: string, + ) => Promise<{ share: SharedSessionRef; displayName: string } | null>; + recordJoin: (shareId: string) => Promise<void>; + /** Joins the socket to a collab room's event fan-out. False when not a member. */ + subscribeRoom: (roomId: string, socket: unknown) => Promise<boolean>; + unsubscribeRoom: (socket: unknown, roomId?: string) => Promise<void>; +} + +/** + * Guest joins through a share link or a room guest link. A guest has no user, + * so the per-call RBAC check a service runs cannot apply: the token is the + * authority. Published on ctx.registry as SESSION_GUESTS_KEY instead. + */ +export interface SessionGuestsV1 { + resolve: (request: { + shareToken?: string; + roomGuestToken?: string; + clientIp: string; + }) => Promise< + { ok: true; share: SharedSessionRef } | { ok: false; reason: string } + >; + recordJoin: (shareId: string) => Promise<void>; +} + +export const SESSION_GUESTS_KEY = "sessions.sharing.guests"; + +export interface RecordingSink { + /** Appends one batch; the first batch starts with the asciicast header. */ + append: (chunk: string) => Promise<void>; + /** Writes or updates the recording row. */ + persist: (summary: { + endedAt: number; + durationSeconds: number; + terminatedByOwner: boolean; + terminationReason: string | null; + }) => Promise<void>; + /** Nothing was recorded; drop whatever was set up. */ + discard: () => void; +} + +export interface RecordingsWriterV1 { + /** Null when recording is off for this user or host. */ + open: (meta: { + sessionId: string; + hostId: number; + userId: string; + protocol: "ssh"; + format: "asciicast"; + startedAt: number; + }) => Promise<RecordingSink | null>; +} diff --git a/src/backend/hosts/terminal/session-manager.ts b/plugins/ssh-terminal/src/backend/session-manager.ts similarity index 83% rename from src/backend/hosts/terminal/session-manager.ts rename to plugins/ssh-terminal/src/backend/session-manager.ts index f009651f3..06fcf5e20 100644 --- a/src/backend/hosts/terminal/session-manager.ts +++ b/plugins/ssh-terminal/src/backend/session-manager.ts @@ -1,17 +1,11 @@ +import { randomUUID } from "crypto"; import { type Client, type ClientChannel } from "ssh2"; import { WebSocket } from "ws"; -import fs from "fs"; -import path from "path"; -import { sshLogger } from "../../utils/logger.js"; -import { - getCurrentSettingValue, - createCurrentSessionRecordingRepository, -} from "../../database/repositories/factory.js"; +import type { TerminalLogger } from "./helpers.js"; +import type { RecordingSink, RecordingsWriterV1 } from "./services.js"; const MAX_BUFFER_BYTES = 512 * 1024; -const DATA_DIR = process.env.DATA_DIR ?? "./db/data"; -const SESSION_LOGS_DIR = path.join(DATA_DIR, "session_logs"); -const DEFAULT_TIMEOUT_MINUTES = 30; +export const DEFAULT_TIMEOUT_MINUTES = 30; const HEALTH_CHECK_INTERVAL_MS = 60_000; const MAX_SESSIONS_PER_USER = 10; // Coalesces recording writes: a chatty SSH stream can emit dozens of "data" @@ -55,10 +49,12 @@ export interface TerminalSession { outputBuffer: string[]; outputBufferBytes: number; - recordingPath: string | null; + /** Output listeners from the sessions.live service. */ + dataListeners: Set<(data: string) => void>; + /** Resolves once the recordings service answered; null when off. */ + recordingSink: Promise<RecordingSink | null> | null; recordingHeader: string | null; recordingBytes: number; - recordingId: number | null; recordingWriteChain: Promise<void>; recordingPersistChain: Promise<void>; pendingRecordingData: string; @@ -96,25 +92,27 @@ export function isMessageAllowedForParticipant( return true; } -class TerminalSessionManager { - private static instance: TerminalSessionManager; +export interface SessionManagerDeps { + log: TerminalLogger; + /** Minutes a detached session is kept; read on every detach. */ + getTimeoutMinutes: () => number; + /** The recordings.writer service, when a plugin provides it. */ + getRecordings: () => RecordingsWriterV1 | null; +} + +export class TerminalSessionManager { private sessions = new Map<string, TerminalSession>(); private healthCheckTimer: NodeJS.Timeout | null = null; + private readonly log: TerminalLogger; - private constructor() { + constructor(private readonly deps: SessionManagerDeps) { + this.log = deps.log; this.healthCheckTimer = setInterval( () => this.healthCheck(), HEALTH_CHECK_INTERVAL_MS, ); } - static getInstance(): TerminalSessionManager { - if (!TerminalSessionManager.instance) { - TerminalSessionManager.instance = new TerminalSessionManager(); - } - return TerminalSessionManager.instance; - } - createSession( userId: string, hostId: number, @@ -149,7 +147,7 @@ class TerminalSessionManager { !existing.sshStream.destroyed; if (isLiveSession) { // Don't destroy a live session (even if detached) — the caller should attach instead - sshLogger.warn( + this.log.warn( "Tab instance has live session, skipping duplicate create", { operation: "session_tab_duplicate_skip", @@ -160,7 +158,7 @@ class TerminalSessionManager { ); return existing.id; } - sshLogger.warn("Tab instance already has session, destroying old", { + this.log.warn("Tab instance already has session, destroying old", { operation: "session_tab_duplicate_cleanup", existingSessionId: existing.id, tabInstanceId, @@ -169,13 +167,34 @@ class TerminalSessionManager { } } - const id = crypto.randomUUID(); + const id = randomUUID(); const now = Date.now(); - let recordingPath: string | null = null; + let recordingSink: Promise<RecordingSink | null> | null = null; let recordingHeader: string | null = null; - if (sessionLoggingEnabled) { - const userLogDir = path.join(SESSION_LOGS_DIR, userId); - recordingPath = path.join(userLogDir, `${id}.cast`); + const recordings = sessionLoggingEnabled ? this.deps.getRecordings() : null; + if (recordings) { + // Events recorded before this resolves wait in pendingRecordingData. + recordingSink = Promise.resolve() + .then(() => + recordings.open({ + sessionId: id, + hostId, + userId, + protocol: "ssh", + format: "asciicast", + startedAt: now, + }), + ) + .catch((err) => { + this.log.warn("Could not start a session recording", { + operation: "session_recording_open_error", + sessionId: id, + error: err instanceof Error ? err.message : String(err), + }); + return null; + }); + } + if (recordingSink) { recordingHeader = `${JSON.stringify({ version: 2, width: cols, @@ -202,16 +221,16 @@ class TerminalSessionManager { detachTimeout: null, outputBuffer: [], outputBufferBytes: 0, - recordingPath, + dataListeners: new Set(), + recordingSink, recordingHeader, recordingBytes: 0, - recordingId: null, recordingWriteChain: Promise.resolve(), recordingPersistChain: Promise.resolve(), pendingRecordingData: "", recordingFlushTimer: null, tmuxSessionName: null, - sessionLoggingEnabled, + sessionLoggingEnabled: !!recordingSink, sessionStartedAt: now, lastPersistedBytes: 0, terminatedByOwner: false, @@ -219,7 +238,7 @@ class TerminalSessionManager { }; this.sessions.set(id, session); - sshLogger.info("Terminal session created", { + this.log.info("Terminal session created", { operation: "session_created", sessionId: id, userId, @@ -276,7 +295,7 @@ class TerminalSessionManager { ): TerminalSession | null { const session = this.sessions.get(sessionId); if (!session) { - sshLogger.warn("Session not found for attachment", { + this.log.warn("Session not found for attachment", { operation: "session_attach_not_found", sessionId, userId, @@ -284,7 +303,7 @@ class TerminalSessionManager { return null; } if (session.userId !== userId) { - sshLogger.warn("Session userId mismatch", { + this.log.warn("Session userId mismatch", { operation: "session_attach_user_mismatch", sessionId, expectedUserId: session.userId, @@ -293,7 +312,7 @@ class TerminalSessionManager { return null; } if (!session.isConnected) { - sshLogger.warn("Session not connected", { + this.log.warn("Session not connected", { operation: "session_attach_not_connected", sessionId, userId, @@ -316,7 +335,7 @@ class TerminalSessionManager { session.tabInstanceId && tabInstanceId ) { - sshLogger.warn("Session actively attached to different tab instance", { + this.log.warn("Session actively attached to different tab instance", { operation: "session_attach_instance_conflict", sessionId, sessionInstanceId: session.tabInstanceId, @@ -341,7 +360,7 @@ class TerminalSessionManager { tabInstanceId && session.tabInstanceId !== tabInstanceId ) { - sshLogger.info( + this.log.info( "Session attached to different tab instance (split-screen)", { operation: "session_attach_split_screen", @@ -373,7 +392,7 @@ class TerminalSessionManager { session.detachTimeout = null; } - const participantId = crypto.randomUUID(); + const participantId = randomUUID(); session.participants.set(participantId, { ws, userId, @@ -385,7 +404,7 @@ class TerminalSessionManager { session.lastDetachedAt = null; this.broadcastParticipants(sessionId); - sshLogger.info("WebSocket attached to session", { + this.log.info("WebSocket attached to session", { operation: "session_attach", sessionId, userId, @@ -414,7 +433,7 @@ class TerminalSessionManager { const session = this.sessions.get(sessionId); if (!session || !session.isConnected) return null; - const participantId = crypto.randomUUID(); + const participantId = randomUUID(); session.participants.set(participantId, { ws, userId: opts.userId, @@ -427,7 +446,7 @@ class TerminalSessionManager { }); this.broadcastParticipants(sessionId); - sshLogger.info("Participant joined shared session", { + this.log.info("Participant joined shared session", { operation: "session_join_participant", sessionId, userId: opts.userId, @@ -558,7 +577,7 @@ class TerminalSessionManager { if (participant.ws === ws && !participant.isOwner) { session.participants.delete(id); this.broadcastParticipants(sessionId); - sshLogger.info("Participant left shared session", { + this.log.info("Participant left shared session", { operation: "session_leave_participant", sessionId, userId: participant.userId, @@ -577,7 +596,7 @@ class TerminalSessionManager { session.terminatedByOwner = true; session.terminationReason = reason; - sshLogger.info("Owner ended shared session", { + this.log.info("Owner ended shared session", { operation: "session_owner_end", sessionId, reason, @@ -608,7 +627,7 @@ class TerminalSessionManager { const timeoutMs = this.getTimeoutMs(); session.detachTimeout = setTimeout(() => { - sshLogger.info("Session idle timeout expired", { + this.log.info("Session idle timeout expired", { operation: "session_idle_timeout", sessionId, userId: session.userId, @@ -616,7 +635,7 @@ class TerminalSessionManager { this.destroySession(sessionId); }, timeoutMs); - sshLogger.info("WebSocket detached from session", { + this.log.info("WebSocket detached from session", { operation: "session_detach", sessionId, userId: session.userId, @@ -634,9 +653,10 @@ class TerminalSessionManager { } this.maybePersistLog(session, true); - if (session.recordingPath && session.recordingBytes === 0) { - fs.promises.unlink(session.recordingPath).catch(() => {}); + if (session.recordingSink && session.recordingBytes === 0) { + void session.recordingSink.then((sink) => sink?.discard()); } + session.dataListeners.clear(); for (const participant of session.participants.values()) { if (participant.isOwner) continue; @@ -688,7 +708,7 @@ class TerminalSessionManager { this.sessions.delete(sessionId); - sshLogger.info("Terminal session destroyed", { + this.log.info("Terminal session destroyed", { operation: "session_destroyed", sessionId, userId: session.userId, @@ -709,7 +729,7 @@ class TerminalSessionManager { session.recordingPersistChain = session.recordingPersistChain .then(() => this.persistSessionLog(session)) .catch((err) => { - sshLogger.warn("Failed to persist session log", { + this.log.warn("Failed to persist session log", { operation: "session_log_persist_error", sessionId: session.id, error: err instanceof Error ? err.message : String(err), @@ -718,44 +738,29 @@ class TerminalSessionManager { } private async persistSessionLog(session: TerminalSession): Promise<void> { - if (!session.recordingPath) return; + if (!session.recordingSink) return; await session.recordingWriteChain; + const sink = await session.recordingSink; + if (!sink) return; const endedAt = Date.now(); const duration = Math.floor((endedAt - session.sessionStartedAt) / 1000); try { - const repo = createCurrentSessionRecordingRepository(); - if (session.recordingId == null) { - const created = await repo.create({ - hostId: session.hostId, - userId: session.userId, - startedAt: new Date(session.sessionStartedAt).toISOString(), - endedAt: new Date(endedAt).toISOString(), - duration, - recordingPath: session.recordingPath, - protocol: "ssh", - format: "asciicast", - terminatedByOwner: session.terminatedByOwner || undefined, - terminationReason: session.terminationReason ?? undefined, - }); - session.recordingId = created.id; - } else { - await repo.updateEnded(session.recordingId, { - endedAt: new Date(endedAt).toISOString(), - duration, - terminatedByOwner: session.terminatedByOwner || undefined, - terminationReason: session.terminationReason ?? undefined, - }); - } + await sink.persist({ + endedAt, + durationSeconds: duration, + terminatedByOwner: session.terminatedByOwner, + terminationReason: session.terminationReason, + }); } catch (err) { - sshLogger.warn("Failed to insert session recording row", { + this.log.warn("Failed to insert session recording row", { operation: "session_recording_insert_error", sessionId: session.id, error: err instanceof Error ? err.message : String(err), }); } - sshLogger.info("Session log persisted", { + this.log.info("Session log persisted", { operation: "session_log_persisted", sessionId: session.id, userId: session.userId, @@ -790,6 +795,14 @@ class TerminalSessionManager { if (removed) session.outputBufferBytes -= removed.length; } + for (const listener of session.dataListeners) { + try { + listener(data); + } catch { + // A listener that throws must not break the session. + } + } + this.recordSessionEvent(session, "o", data); } @@ -810,7 +823,7 @@ class TerminalSessionManager { type: "i" | "o" | "r", data: string, ): void { - if (!session.sessionLoggingEnabled || !session.recordingPath || !data) + if (!session.sessionLoggingEnabled || !session.recordingSink || !data) return; const elapsed = (Date.now() - session.sessionStartedAt) / 1000; const line = `${JSON.stringify([elapsed, type, data])}\n`; @@ -825,27 +838,33 @@ class TerminalSessionManager { } } - /** Coalesces buffered recording lines into a single disk write. */ + /** + * Coalesces buffered recording lines into one write, chained so they land + * in order. Never one write per chunk: that starved the libuv threadpool + * (issue #1049). + */ private flushRecording(session: TerminalSession): void { - if (!session.recordingPath || !session.pendingRecordingData) return; + const pendingSink = session.recordingSink; + if (!pendingSink || !session.pendingRecordingData) return; const chunk = session.pendingRecordingData; session.pendingRecordingData = ""; const firstWrite = session.recordingBytes === Buffer.byteLength(chunk); - session.recordingWriteChain = session.recordingWriteChain.then(async () => { - if (firstWrite) { - await fs.promises.mkdir(path.dirname(session.recordingPath!), { - recursive: true, - }); - await fs.promises.writeFile( - session.recordingPath!, - `${session.recordingHeader}${chunk}`, - "utf8", + session.recordingWriteChain = session.recordingWriteChain + .then(async () => { + const sink = await pendingSink; + if (!sink) return; + await sink.append( + firstWrite ? `${session.recordingHeader}${chunk}` : chunk, ); - return; - } - await fs.promises.appendFile(session.recordingPath!, chunk, "utf8"); - }); + }) + .catch((err) => { + this.log.warn("Failed to write session recording", { + operation: "session_recording_write_error", + sessionId: session.id, + error: err instanceof Error ? err.message : String(err), + }); + }); } flushBuffer(session: TerminalSession): string | null { @@ -862,18 +881,12 @@ class TerminalSessionManager { } private getTimeoutMs(): number { - try { - const value = getCurrentSettingValue("terminal_session_timeout_minutes"); - if (value) { - const minutes = parseInt(value, 10); - if (!isNaN(minutes) && minutes > 0) { - return minutes * 60_000; - } - } - } catch { - // DB not available, use default - } - return DEFAULT_TIMEOUT_MINUTES * 60_000; + const minutes = this.deps.getTimeoutMinutes(); + return ( + (Number.isFinite(minutes) && minutes > 0 + ? minutes + : DEFAULT_TIMEOUT_MINUTES) * 60_000 + ); } private healthCheck(): void { @@ -897,7 +910,7 @@ class TerminalSessionManager { : 0; if (detachedDuration > GRACE_PERIOD_MS) { - sshLogger.info( + this.log.info( "SSH stream destroyed during detach window, cleaning up", { operation: "session_health_check_stream_destroyed", @@ -930,5 +943,3 @@ class TerminalSessionManager { } } } - -export const sessionManager = TerminalSessionManager.getInstance(); diff --git a/plugins/ssh-terminal/src/backend/settings.ts b/plugins/ssh-terminal/src/backend/settings.ts new file mode 100644 index 000000000..d6004e857 --- /dev/null +++ b/plugins/ssh-terminal/src/backend/settings.ts @@ -0,0 +1,91 @@ +import type { PluginContext } from "@termix/plugin-sdk/backend"; +import { + normalizeTouchInputSettings, + type TouchInputSettings, +} from "../shared/touch-input-settings.js"; +import { + resolveTerminalImageStorageSettings, + TERMINAL_IMAGE_STORAGE_KEYS, + type TerminalImageStorageSettings, +} from "./images/image-storage-settings.js"; +import { DEFAULT_TIMEOUT_MINUTES } from "./session-manager.js"; + +/** + * Admin setting keys, as declared in manifest.json. The image storage fields + * have no default on purpose: an unset field falls through to the old + * TERMIX_IMAGE_* environment variables, the way the settings table did. + */ +export const ADMIN_KEYS = { + sessionTimeoutMinutes: "sessionTimeoutMinutes", + sessionPersistence: "sessionPersistence", + commandHistoryEnabled: "commandHistoryEnabled", + touchInput: "touchInput", + imageStorageMode: "imageStorageMode", + imageLocalDir: "imageLocalDir", + imageHostPath: "imageHostPath", + imageTtlMs: "imageTtlMs", + imageMaxCount: "imageMaxCount", + imageMaxBytes: "imageMaxBytes", +} as const; + +export const HOST_KEYS = { + enableTerminal: "enableTerminal", + enableTerminalToolbar: "enableTerminalToolbar", + enableCommandHistory: "enableCommandHistory", +} as const; + +const IMAGE_FIELD_BY_LEGACY_KEY: Record<string, string> = { + [TERMINAL_IMAGE_STORAGE_KEYS.mode]: ADMIN_KEYS.imageStorageMode, + [TERMINAL_IMAGE_STORAGE_KEYS.localDir]: ADMIN_KEYS.imageLocalDir, + [TERMINAL_IMAGE_STORAGE_KEYS.hostPath]: ADMIN_KEYS.imageHostPath, + [TERMINAL_IMAGE_STORAGE_KEYS.ttlMs]: ADMIN_KEYS.imageTtlMs, + [TERMINAL_IMAGE_STORAGE_KEYS.maxCount]: ADMIN_KEYS.imageMaxCount, + [TERMINAL_IMAGE_STORAGE_KEYS.maxBytes]: ADMIN_KEYS.imageMaxBytes, +}; + +export async function readImageStorageSettings( + ctx: PluginContext, +): Promise<TerminalImageStorageSettings> { + return resolveTerminalImageStorageSettings({ + get: async (legacyKey) => { + const field = IMAGE_FIELD_BY_LEGACY_KEY[legacyKey]; + if (!field) return null; + const value = await ctx.settings.get(field); + if (value === undefined || value === null || value === "") return null; + return String(value); + }, + warn: (message) => ctx.log.warn(message), + }); +} + +export interface ClientSettings { + sessionTimeoutMinutes: number; + sessionPersistence: boolean; + commandHistoryEnabled: boolean; + touchInput: TouchInputSettings; +} + +/** What every signed-in user's terminal needs, whether or not they are an admin. */ +export async function readClientSettings( + ctx: PluginContext, +): Promise<ClientSettings> { + const all = await ctx.settings.getAll("admin"); + const timeout = Number(all[ADMIN_KEYS.sessionTimeoutMinutes]); + let touch: unknown = all[ADMIN_KEYS.touchInput]; + if (typeof touch === "string") { + try { + touch = JSON.parse(touch); + } catch { + touch = null; + } + } + return { + sessionTimeoutMinutes: + Number.isFinite(timeout) && timeout > 0 + ? timeout + : DEFAULT_TIMEOUT_MINUTES, + sessionPersistence: all[ADMIN_KEYS.sessionPersistence] !== false, + commandHistoryEnabled: all[ADMIN_KEYS.commandHistoryEnabled] !== false, + touchInput: normalizeTouchInputSettings(touch), + }; +} diff --git a/plugins/ssh-terminal/src/backend/tables.ts b/plugins/ssh-terminal/src/backend/tables.ts new file mode 100644 index 000000000..7636e88ad --- /dev/null +++ b/plugins/ssh-terminal/src/backend/tables.ts @@ -0,0 +1,39 @@ +import { + adoptLegacyTable, + defineTable, + id, + refHost, + refUser, + text, + timestamp, +} from "@termix/plugin-sdk/db"; + +/** + * Commands typed into terminal sessions, per user and host. + * + * Adopted from core's command_history, so the column and index names are the + * legacy ones: the rename carries the existing rows and index across. + */ +export const commandHistory = adoptLegacyTable( + "command_history", + defineTable( + "command_history", + { + id: id(), + userId: refUser(), + hostId: refHost(), + command: text().notNull(), + executedAt: timestamp().notNull().defaultNow(), + }, + { + indexes: [ + { + name: "idx_command_history_user_host", + columns: ["userId", "hostId"], + }, + ], + }, + ), +); + +export const tables = [commandHistory]; diff --git a/plugins/ssh-terminal/src/backend/terminal-socket.ts b/plugins/ssh-terminal/src/backend/terminal-socket.ts new file mode 100644 index 000000000..cbb3335e9 --- /dev/null +++ b/plugins/ssh-terminal/src/backend/terminal-socket.ts @@ -0,0 +1,2974 @@ +import { AsyncResource } from "async_hooks"; +import { StringDecoder } from "string_decoder"; +import { WebSocket, type RawData } from "ws"; +import ssh2Pkg, { + type Client as SSHClientType, + type ClientChannel, + type PseudoTtyOptions, +} from "ssh2"; +import { + PluginSshInteractionError, + type PluginContext, + type PluginSshHost, + type PluginSshPrepared, + type PluginWebSocketConnection, +} from "@termix/plugin-sdk/backend"; +import { + asObject, + asString, + getErrorMessage, + hostAddressMismatch, + HOST_ADDRESS_MISMATCH_MESSAGE, + HOST_NOT_ON_THIS_SERVER_MESSAGE, + isRetriableDnsError, + isWindowsSftpPath, + MemoryAgent, + parseWsMessage, + resolveHostForSshConnect, + resolveServerHostId, + resolveServerJumpHosts, + sftpPathToLocalPath, + toTerminalDimension, + type TerminalLogger, +} from "./helpers.js"; +import { SSHAuthManager } from "./keyboard-prompt.js"; +import { + isMessageAllowedForParticipant, + type TerminalSessionManager, +} from "./session-manager.js"; +import type { + SessionGuestsV1, + SessionSharingV1, + SharedSessionRef, + TmuxSessionsV1, +} from "./services.js"; + +const { Client, utils: ssh2Utils } = ssh2Pkg; + +interface ConnectToHostData { + cols: number; + rows: number; + hostConfig: { + id: number; + /** Names the host across a sync pair; `id` only names it locally. */ + syncId?: string | null; + instanceId?: string; + ip: string; + port: number; + username: string; + password?: string; + key?: string; + keyPassword?: string; + keyType?: string; + authType?: string; + credentialId?: number; + userId?: string; + forceKeyboardInteractive?: boolean; + useWarpgate?: boolean; + jumpHosts?: Array<{ hostId: number }>; + useSocks5?: boolean; + socks5Host?: string; + socks5Port?: number; + socks5Username?: string; + socks5Password?: string; + socks5ProxyChain?: unknown; + portKnockSequence?: Array<{ + port: number; + protocol?: "tcp" | "udp"; + delay?: number; + }>; + terminalConfig?: { + keepaliveInterval?: number; + keepaliveCountMax?: number; + [key: string]: unknown; + }; + enableSessionLogging?: boolean; + /** When true, ignore key material and force password auth (fallback path). */ + passwordFallbackOnly?: boolean; + }; + initialPath?: string; + executeCommand?: string; + /** Attach straight to this tmux session once the shell is ready + * (tmux monitor opens its panes through a real PTY this way). */ + tmuxAttachSession?: string; +} + +interface ResizeData { + cols: number; + rows: number; +} + +interface TOTPResponseData { + code?: string; +} + +export interface TerminalSocketDeps { + ctx: PluginContext; + log: TerminalLogger; + sessionManager: TerminalSessionManager; + /** The optional tmux.sessions service, as the acting user. */ + getTmux: () => TmuxSessionsV1 | null; + /** The optional sessions.sharing service, as the acting user. */ + getSharing: () => SessionSharingV1 | null; + /** Guest link resolution, published on ctx.registry by session sharing. */ + getGuests: () => SessionGuestsV1 | null; +} + +/** + * Serves /plugin-ws/ssh-terminal/terminal. + * + * The route is public with optional auth: a signed-in user arrives with a + * token core verifies, and a share-link or room guest arrives with a token of + * its own that is resolved here. Socket and ssh2 listeners are bound with + * AsyncResource so every ctx call inside them still runs as the socket's user. + */ +export function createTerminalSocket(deps: TerminalSocketDeps) { + const { ctx, log: sshLogger, sessionManager } = deps; + const authLogger = sshLogger; + const bind = <A extends unknown[], R>(fn: (...args: A) => R) => + AsyncResource.bind(fn); + const sockets = new Set<WebSocket>(); + + function closeAll(): void { + for (const ws of sockets) { + try { + ws.close(1001, "Terminal plugin disabled"); + } catch { + // Already gone. + } + } + sockets.clear(); + } + + /** Joins an anonymous guest socket to a live shared session, read-only or not per the share. */ + async function attachShareGuest( + ws: WebSocket, + share: SharedSessionRef, + guests: SessionGuestsV1, + ): Promise<void> { + const session = sessionManager.getSession(share.sessionId); + if (!session || !session.isConnected) { + ws.close(1008, "Session has ended"); + return; + } + + const joined = sessionManager.joinAsParticipant(share.sessionId, ws, { + userId: null, + permissionLevel: share.permissionLevel, + guestLabel: "Guest", + shareId: share.id, + }); + if (!joined) { + ws.close(1008, "Session is no longer active"); + return; + } + + guests.recordJoin(share.id).catch(() => {}); + + const buffered = sessionManager.getBuffer(joined); + if (buffered) { + ws.send(JSON.stringify({ type: "data", data: buffered })); + } + ws.send( + JSON.stringify({ type: "sessionAttached", sessionId: share.sessionId }), + ); + ws.send(JSON.stringify({ type: "connected", message: "Joined session" })); + + const currentSessionId: string = share.sessionId; + + let wsAlive = true; + ws.on("pong", () => { + wsAlive = true; + }); + const wsPingInterval = setInterval(() => { + if (ws.readyState === WebSocket.OPEN) { + if (!wsAlive) { + ws.terminate(); + return; + } + wsAlive = false; + ws.ping(); + } else { + clearInterval(wsPingInterval); + } + }, 30000); + + ws.on("close", () => { + clearInterval(wsPingInterval); + sessionManager.removeParticipant(currentSessionId, ws); + sshLogger.info("Guest left shared terminal session", { + operation: "terminal_guest_disconnect", + sessionId: currentSessionId, + shareId: share.id, + }); + }); + + ws.on("message", (msg: RawData) => { + let type: string; + let data: unknown; + try { + ({ type, data } = parseWsMessage(msg)); + } catch { + return; + } + + const liveSession = sessionManager.getSession(currentSessionId); + const participant = liveSession + ? sessionManager.getParticipantForWs(liveSession, ws) + : null; + if (!isMessageAllowedForParticipant(participant, type)) { + return; + } + + switch (type) { + case "input": { + if (typeof data !== "string") break; + const inputData = data; + sessionManager.bufferInput(currentSessionId, inputData); + const inputStream = liveSession?.sshStream; + if (inputStream) { + try { + inputStream.write(Buffer.from(inputData, "utf8")); + } catch { + inputStream.write(Buffer.from(inputData, "latin1")); + } + } + break; + } + case "ping": + ws.send(JSON.stringify({ type: "pong" })); + break; + case "disconnect": + sessionManager.removeParticipant(currentSessionId, ws); + break; + default: + break; + } + }); + } + + /** + * A share-link (?shareToken) or collab room (?roomGuestToken) guest. Never + * touches user credentials: guests join an already-live stream. Session + * sharing resolves the token; without it guest links are refused. + */ + async function handleGuestConnection( + ws: WebSocket, + connection: PluginWebSocketConnection, + tokens: { shareToken?: string; roomGuestToken?: string }, + ): Promise<void> { + const guests = deps.getGuests(); + if (!guests) { + ws.close(1008, "Session sharing is not available"); + return; + } + const resolved = await guests.resolve({ + ...tokens, + clientIp: connection.clientIp, + }); + if ("reason" in resolved) { + ws.close(1008, resolved.reason); + return; + } + return attachShareGuest(ws, resolved.share, guests); + } + + async function handleConnection( + connection: PluginWebSocketConnection, + ): Promise<void> { + const ws = connection.socket as WebSocket; + const req = connection.request as import("http").IncomingMessage; + const sessionId: string | undefined = undefined; + sockets.add(ws); + ws.once("close", () => sockets.delete(ws)); + + ws.on("error", (error) => { + sshLogger.error("WebSocket connection error", error, { + operation: "ws_error", + sessionId, + }); + }); + + const urlObj = new URL(req.url || "", "http://localhost"); + const shareToken = urlObj.searchParams.get("shareToken"); + + if (shareToken) { + await handleGuestConnection(ws, connection, { shareToken }); + return; + } + const roomGuestToken = urlObj.searchParams.get("roomGuestToken"); + if (roomGuestToken) { + await handleGuestConnection(ws, connection, { roomGuestToken }); + return; + } + + if (!connection.userId) { + ws.close(1008, "Authentication required"); + return; + } + const userId = connection.userId; + + if (!connection.isDataUnlocked()) { + ws.send( + JSON.stringify({ + type: "error", + message: "Data locked - re-authenticate with password", + code: "DATA_LOCKED", + }), + ); + ws.close(1008, "Data access required"); + return; + } + + sshLogger.info("Terminal WebSocket connection established", { + operation: "terminal_ws_connect", + sessionId, + userId, + }); + + let currentSessionId: string | null = null; + let sshConn: SSHClientType | null = null; + let sshStream: ClientChannel | null = null; + let lastJumpClient: SSHClientType | null = null; + let keyboardInteractiveFinish: ((responses: string[]) => void) | null = + null; + let totpPromptSent = false; + let totpTimeout: NodeJS.Timeout | null = null; + let isKeyboardInteractive = false; + let keyboardInteractiveResponded = false; + let isConnecting = false; + let isConnected = false; + let isCleaningUp = false; + let isShellInitializing = false; + let isDuplicateConnDiscarded = false; + let warpgateAuthPromptSent = false; + let warpgateAuthTimeout: NodeJS.Timeout | null = null; + let isAwaitingAuthCredentials = false; + + let wsAlive = true; + + ws.on("pong", () => { + wsAlive = true; + }); + + const wsPingInterval = setInterval(() => { + if (ws.readyState === WebSocket.OPEN) { + if (!wsAlive) { + sshLogger.warn( + "WebSocket pong timeout - terminating zombie connection", + { + operation: "ws_pong_timeout", + userId, + sessionId: currentSessionId, + }, + ); + ws.terminate(); + return; + } + wsAlive = false; + ws.ping(); + } + }, 30000); + + ws.on( + "close", + bind(() => { + clearInterval(wsPingInterval); + void deps + .getSharing() + ?.unsubscribeRoom(ws) + .catch(() => {}); + sshLogger.info("Terminal WebSocket disconnected", { + operation: "terminal_ws_disconnect", + sessionId, + userId, + }); + + if (currentSessionId) { + const session = sessionManager.getSession(currentSessionId); + if (session?.isConnected) { + const participant = sessionManager.getParticipantForWs(session, ws); + if (participant && !participant.isOwner) { + sessionManager.removeParticipant(currentSessionId, ws); + } else { + // Only detach if this WS is still the owner's attached socket, or + // no owner is currently attached. If a refresh reconnected and + // reattached a new WS before this close event fired, we must not + // clobber that new attachment. + const ownerStillAttached = Array.from( + session.participants.values(), + ).some((p) => p.isOwner && p.ws !== ws); + if (!ownerStillAttached) { + sessionManager.detachWs(currentSessionId); + } + } + } else { + sessionManager.destroySession(currentSessionId); + currentSessionId = null; + } + } + cleanupAuthState(); + }), + ); + + function resetConnectionState() { + isConnecting = false; + isConnected = false; + isKeyboardInteractive = false; + keyboardInteractiveResponded = false; + keyboardInteractiveFinish = null; + totpPromptSent = false; + warpgateAuthPromptSent = false; + } + + ws.on( + "message", + bind(async (msg: RawData) => { + if (!connection.isDataUnlocked()) { + ws.send( + JSON.stringify({ + type: "error", + message: "Data access expired - please re-authenticate", + code: "DATA_EXPIRED", + }), + ); + ws.close(1008, "Data access expired"); + return; + } + + let type: string; + let data: unknown; + try { + ({ type, data } = parseWsMessage(msg)); + } catch (e) { + sshLogger.warn("Rejected malformed WebSocket message", { + operation: "websocket_message_invalid", + userId, + error: getErrorMessage(e), + }); + ws.send( + JSON.stringify({ type: "error", message: "Invalid message" }), + ); + return; + } + + // Server-side gate: non-owner participants (read-only or read-write + // guests/joiners) may only send input/ping/disconnect - everything else + // (auth flows, tmux, resize, etc.) is owner-only and silently ignored. + if (type !== "joinSharedSession") { + const gateSession = currentSessionId + ? sessionManager.getSession(currentSessionId) + : null; + const gateParticipant = gateSession + ? sessionManager.getParticipantForWs(gateSession, ws) + : null; + if (!isMessageAllowedForParticipant(gateParticipant, type)) { + return; + } + } + + try { + switch (type) { + case "connectToHost": { + const connectData = data as ConnectToHostData; + if (!connectData?.hostConfig) { + ws.send( + JSON.stringify({ + type: "error", + message: "Missing host configuration", + }), + ); + break; + } + connectData.hostConfig.userId = userId; + handleConnectToHost(connectData).catch((error) => { + const errMsg = getErrorMessage(error); + if ( + errMsg.includes("Cannot parse privateKey") && + errMsg.includes("no passphrase") + ) { + isAwaitingAuthCredentials = true; + ws.send( + JSON.stringify({ + type: "passphrase_required", + message: + "The SSH key is encrypted. Please enter the passphrase to unlock it.", + }), + ); + return; + } + sshLogger.error("Failed to connect to host", error, { + operation: "ssh_connect", + userId, + hostId: connectData.hostConfig?.id, + ip: connectData.hostConfig?.ip, + }); + ws.send( + JSON.stringify({ + type: "error", + message: "Failed to connect to host: " + errMsg, + }), + ); + }); + break; + } + + case "attachSession": { + const attachData = data as { + sessionId: string; + cols: number; + rows: number; + tabInstanceId?: string; + }; + sshLogger.info("Attempting to attach session", { + operation: "terminal_attach_session", + sessionId: attachData.sessionId, + tabInstanceId: attachData.tabInstanceId, + userId, + requestedCols: attachData.cols, + requestedRows: attachData.rows, + }); + const session = sessionManager.attachWs( + attachData.sessionId, + userId, + ws, + attachData.tabInstanceId, + ); + if (session) { + sshLogger.success("Session attached successfully", { + operation: "terminal_attach_success", + sessionId: attachData.sessionId, + sessionCreatedAt: session.createdAt, + wasDetached: !!session.lastDetachedAt, + detachedDuration: session.lastDetachedAt + ? Date.now() - session.lastDetachedAt + : 0, + }); + currentSessionId = attachData.sessionId; + sshStream = session.sshStream; + sshConn = session.sshConn; + isConnecting = false; + isConnected = true; + const buffered = sessionManager.getBuffer(session); + if (buffered) { + ws.send(JSON.stringify({ type: "data", data: buffered })); + } + const attachCols = toTerminalDimension(attachData.cols); + const attachRows = toTerminalDimension(attachData.rows); + if ( + attachCols && + attachRows && + (attachCols !== session.cols || attachRows !== session.rows) + ) { + session.sshStream?.setWindow( + attachRows, + attachCols, + attachRows, + attachCols, + ); + session.cols = attachCols; + session.rows = attachRows; + } + + ws.send( + JSON.stringify({ + type: "sessionAttached", + sessionId: attachData.sessionId, + }), + ); + ws.send( + JSON.stringify({ + type: "connected", + message: "Session reattached", + }), + ); + } else { + sshLogger.warn( + "Session attachment failed - will create new connection", + { + operation: "terminal_attach_failed", + sessionId: attachData.sessionId, + tabInstanceId: attachData.tabInstanceId, + userId, + reason: "session_not_found_or_invalid", + }, + ); + ws.send( + JSON.stringify({ + type: "sessionExpired", + sessionId: attachData.sessionId, + }), + ); + } + break; + } + + case "listSessions": { + const sessions = sessionManager.getUserSessions(userId); + ws.send( + JSON.stringify({ + type: "sessionList", + sessions: sessions.map((s) => ({ + id: s.id, + hostId: s.hostId, + hostName: s.hostName, + createdAt: s.createdAt, + lastDetachedAt: s.lastDetachedAt, + tmuxSessionName: s.tmuxSessionName, + })), + }), + ); + break; + } + + case "resize": { + const resizeData = data as ResizeData; + handleResize(resizeData); + break; + } + + case "disconnect": { + const disconnectSession = currentSessionId + ? sessionManager.getSession(currentSessionId) + : null; + const disconnectParticipant = disconnectSession + ? sessionManager.getParticipantForWs(disconnectSession, ws) + : null; + if (disconnectParticipant && !disconnectParticipant.isOwner) { + if (currentSessionId) { + sessionManager.removeParticipant(currentSessionId, ws); + currentSessionId = null; + } + break; + } + if (currentSessionId) { + sessionManager.destroySession(currentSessionId); + currentSessionId = null; + } + cleanupAuthState(); + sshConn = null; + sshStream = null; + break; + } + + case "get_cwd": { + const activeConn = + sessionManager.getSession(currentSessionId)?.sshConn ?? sshConn; + if (!activeConn) { + ws.send(JSON.stringify({ type: "cwd", path: "/" })); + break; + } + activeConn.exec("pwd", (err, execStream) => { + if (err) { + ws.send(JSON.stringify({ type: "cwd", path: "/" })); + return; + } + let stdout = ""; + execStream.on("data", (chunk: Buffer) => { + stdout += chunk.toString("utf-8"); + }); + execStream.stderr.on("data", () => {}); + execStream.on("close", () => { + const cwd = stdout.trim() || "/"; + if (ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ type: "cwd", path: cwd })); + } + }); + }); + break; + } + + case "open_file_in_editor": { + const requestedPath = asString(asObject(data).path); + const activeConn = + sessionManager.getSession(currentSessionId)?.sshConn ?? sshConn; + if (!activeConn || !requestedPath) { + ws.send( + JSON.stringify({ + type: "open_file_in_editor", + path: requestedPath || "/", + }), + ); + break; + } + const escapedPath = requestedPath.replace(/'/g, "'\\''"); + activeConn.exec( + `realpath '${escapedPath}' 2>/dev/null || echo '${escapedPath}'`, + (err, execStream) => { + if (err) { + ws.send( + JSON.stringify({ + type: "open_file_in_editor", + path: requestedPath, + }), + ); + return; + } + let stdout = ""; + execStream.on("data", (chunk: Buffer) => { + stdout += chunk.toString("utf-8"); + }); + execStream.stderr.on("data", () => {}); + execStream.on("close", () => { + const resolvedPath = stdout.trim() || requestedPath; + if (ws.readyState === WebSocket.OPEN) { + ws.send( + JSON.stringify({ + type: "open_file_in_editor", + path: resolvedPath, + }), + ); + } + }); + }, + ); + break; + } + + case "input": { + if (typeof data !== "string") break; + const inputData = data; + if (currentSessionId) { + sessionManager.bufferInput(currentSessionId, inputData); + } + const inputStream = + sessionManager.getSession(currentSessionId)?.sshStream ?? + sshStream; + if (inputStream) { + if (inputData === "\t") { + inputStream.write(inputData); + } else if ( + typeof inputData === "string" && + inputData.startsWith("\x1b") + ) { + inputStream.write(inputData); + } else { + try { + inputStream.write(Buffer.from(inputData, "utf8")); + } catch (error) { + sshLogger.error( + "Error writing input to SSH stream", + error, + { + operation: "ssh_input_encoding", + userId, + dataLength: inputData.length, + }, + ); + inputStream.write(Buffer.from(inputData, "latin1")); + } + } + } + break; + } + + case "ping": + ws.send(JSON.stringify({ type: "pong" })); + break; + + case "tmux_attach": { + const tmuxData = data as { sessionName: string }; + const session = currentSessionId + ? sessionManager.getSession(currentSessionId) + : null; + const tmux = deps.getTmux(); + if (session?.sshStream && tmux) { + const existingName = tmuxData.sessionName || undefined; + if (existingName) { + void tmux + .attachOrCreate(session.sshStream, existingName) + .catch(() => {}); + session.tmuxSessionName = existingName; + sshLogger.info("User selected tmux session to attach", { + operation: "tmux_user_attach", + sessionName: existingName, + hostId: session.hostId, + }); + ws.send( + JSON.stringify({ + type: "tmux_session_attached", + sessionName: existingName, + }), + ); + } else { + const newName = `termix-${session.hostId}-${Date.now().toString(36).slice(-4)}`; + void tmux + .attachOrCreate(session.sshStream, undefined, newName) + .catch(() => {}); + const sshConn = session.sshConn; + if (sshConn) { + (async () => { + const confirmed = await tmux.waitForSession( + sshConn, + newName, + ); + session.tmuxSessionName = confirmed; + sshLogger.info("User requested new tmux session", { + operation: "tmux_user_create", + sessionName: confirmed, + hostId: session.hostId, + }); + ws.send( + JSON.stringify({ + type: "tmux_session_created", + sessionName: confirmed, + }), + ); + })(); + } + } + } + break; + } + + case "tmux_detach": { + const session = currentSessionId + ? sessionManager.getSession(currentSessionId) + : null; + if (session?.sshConn && session.tmuxSessionName) { + const tmuxName = session.tmuxSessionName; + session.sshStream?.write("\x02d"); + session.tmuxSessionName = null; + sshLogger.info("User detached from tmux session", { + operation: "tmux_user_detach", + sessionName: tmuxName, + hostId: session.hostId, + }); + ws.send( + JSON.stringify({ + type: "tmux_detached", + sessionName: tmuxName, + }), + ); + } + break; + } + + case "totp_response": { + const totpData = data as TOTPResponseData; + if (keyboardInteractiveFinish && totpData?.code) { + if (totpTimeout) { + clearTimeout(totpTimeout); + totpTimeout = null; + } + const totpCode = totpData.code; + keyboardInteractiveFinish([totpCode]); + keyboardInteractiveFinish = null; + totpPromptSent = false; + } else { + sshLogger.warn( + "TOTP response received but no callback available", + { + operation: "totp_response_error", + userId, + hasCallback: !!keyboardInteractiveFinish, + hasCode: !!totpData?.code, + }, + ); + ws.send( + JSON.stringify({ + type: "error", + message: + "TOTP authentication state lost. Please reconnect.", + }), + ); + } + break; + } + + case "password_response": { + const passwordData = data as TOTPResponseData; + if (keyboardInteractiveFinish && passwordData?.code) { + if (totpTimeout) { + clearTimeout(totpTimeout); + totpTimeout = null; + } + const password = passwordData.code; + keyboardInteractiveFinish([password]); + keyboardInteractiveFinish = null; + } else { + sshLogger.warn( + "Password response received but no callback available", + { + operation: "password_response_error", + userId, + hasCallback: !!keyboardInteractiveFinish, + hasCode: !!passwordData?.code, + }, + ); + ws.send( + JSON.stringify({ + type: "error", + message: + "Password authentication state lost. Please reconnect.", + }), + ); + } + break; + } + + case "warpgate_auth_continue": { + if (keyboardInteractiveFinish) { + if (warpgateAuthTimeout) { + clearTimeout(warpgateAuthTimeout); + warpgateAuthTimeout = null; + } + keyboardInteractiveFinish([""]); + keyboardInteractiveFinish = null; + warpgateAuthPromptSent = false; + } + break; + } + + case "reconnect_with_credentials": { + const credentialsData = data as { + cols: number; + rows: number; + hostConfig: ConnectToHostData["hostConfig"]; + password?: string; + sshKey?: string; + keyPassword?: string; + }; + + if (!credentialsData?.hostConfig) { + ws.send( + JSON.stringify({ + type: "error", + message: "Missing host configuration", + }), + ); + break; + } + + if (credentialsData.password) { + credentialsData.hostConfig.password = credentialsData.password; + credentialsData.hostConfig.authType = "password"; + ( + credentialsData.hostConfig as Record<string, unknown> + ).userProvidedPassword = true; + } else if (credentialsData.sshKey) { + credentialsData.hostConfig.key = credentialsData.sshKey; + credentialsData.hostConfig.keyPassword = + credentialsData.keyPassword; + credentialsData.hostConfig.authType = "key"; + } else if (credentialsData.keyPassword) { + credentialsData.hostConfig.keyPassword = + credentialsData.keyPassword; + } + + isAwaitingAuthCredentials = false; + if (currentSessionId) { + sessionManager.destroySession(currentSessionId); + currentSessionId = null; + } + cleanupAuthState(); + sshConn = null; + sshStream = null; + + const reconnectData: ConnectToHostData = { + cols: credentialsData.cols, + rows: credentialsData.rows, + hostConfig: credentialsData.hostConfig, + }; + + handleConnectToHost(reconnectData).catch((error) => { + const errMsg = getErrorMessage(error); + if ( + errMsg.includes("Cannot parse privateKey") && + errMsg.includes("no passphrase") + ) { + isAwaitingAuthCredentials = true; + ws.send( + JSON.stringify({ + type: "passphrase_required", + message: + "The SSH key is encrypted. Please enter the passphrase to unlock it.", + }), + ); + return; + } + sshLogger.error("Failed to reconnect with credentials", error, { + operation: "ssh_reconnect_with_credentials", + userId, + hostId: credentialsData.hostConfig?.id, + ip: credentialsData.hostConfig?.ip, + }); + ws.send( + JSON.stringify({ + type: "error", + message: + "Failed to connect with provided credentials: " + errMsg, + }), + ); + }); + break; + } + + case "opkssh_start_auth": { + const opksshData = data as { hostId: number }; + try { + await startAuthInteraction("opkssh", opksshData.hostId, data); + } catch (error) { + sshLogger.error("Failed to start OPKSSH auth", error, { + operation: "opkssh_start_auth_error", + userId, + hostId: opksshData.hostId, + }); + ws.send( + JSON.stringify({ + type: "opkssh_error", + requestId: "", + error: + error instanceof PluginSshInteractionError + ? error.message + : "Failed to start OPKSSH authentication", + }), + ); + } + break; + } + + case "opkssh_cancel": { + const cancelData = data as { requestId: string }; + try { + await ctx.ssh.cancelInteraction("opkssh", { + requestId: String(cancelData?.requestId ?? ""), + }); + resetConnectionState(); + } catch (error) { + sshLogger.error("Failed to cancel OPKSSH auth", error, { + operation: "opkssh_cancel_error", + userId, + }); + } + break; + } + + case "opkssh_browser_opened": { + break; + } + + case "opkssh_auth_completed": { + const completedData = data as { + hostId: number; + cols?: number; + rows?: number; + hostConfig?: ConnectToHostData["hostConfig"]; + }; + + resetConnectionState(); + + const reconnectConfig: ConnectToHostData = { + cols: completedData.cols || 80, + rows: completedData.rows || 24, + hostConfig: + completedData.hostConfig || + ({ + id: completedData.hostId, + ip: "", + port: 22, + username: "", + userId, + } as ConnectToHostData["hostConfig"]), + }; + + handleConnectToHost(reconnectConfig).catch((error) => { + sshLogger.error( + "Failed to reconnect after OPKSSH auth", + error, + { + operation: "opkssh_reconnect_error", + userId, + hostId: completedData.hostId, + }, + ); + ws.send( + JSON.stringify({ + type: "error", + message: + "Failed to connect after authentication: " + + getErrorMessage(error), + }), + ); + }); + break; + } + + case "vault_start_auth": { + const vaultData = data as { hostId: number }; + try { + await startAuthInteraction("vault", vaultData.hostId, data); + } catch (error) { + sshLogger.error("Failed to start Vault auth", error, { + operation: "vault_start_auth_error", + userId, + hostId: vaultData.hostId, + }); + ws.send( + JSON.stringify({ + type: "vault_error", + hostId: vaultData.hostId, + error: getErrorMessage( + error, + "Failed to start Vault authentication", + ), + }), + ); + } + break; + } + + case "vault_cancel": { + const cancelData = data as { hostId: number }; + try { + await ctx.ssh.cancelInteraction("vault", { + hostId: cancelData.hostId, + }); + resetConnectionState(); + } catch (error) { + sshLogger.error("Failed to cancel Vault auth", error, { + operation: "vault_cancel_error", + userId, + }); + } + break; + } + + case "vault_auth_completed": { + const completedData = data as { + hostId: number; + cols?: number; + rows?: number; + hostConfig?: ConnectToHostData["hostConfig"]; + }; + + resetConnectionState(); + + const reconnectConfig: ConnectToHostData = { + cols: completedData.cols || 80, + rows: completedData.rows || 24, + hostConfig: + completedData.hostConfig || + ({ + id: completedData.hostId, + ip: "", + port: 22, + username: "", + userId, + } as ConnectToHostData["hostConfig"]), + }; + + handleConnectToHost(reconnectConfig).catch((error) => { + sshLogger.error("Failed to reconnect after Vault auth", error, { + operation: "vault_reconnect_error", + userId, + hostId: completedData.hostId, + }); + ws.send( + JSON.stringify({ + type: "error", + message: + "Failed to connect after authentication: " + + getErrorMessage(error), + }), + ); + }); + break; + } + + case "collab_subscribe": { + const { roomId } = (data ?? {}) as { roomId?: string }; + if (typeof roomId !== "string" || !roomId) break; + try { + const sharing = deps.getSharing(); + const joined = sharing + ? await sharing.subscribeRoom(roomId, ws) + : false; + if (!joined) { + ws.send( + JSON.stringify({ + type: "error", + message: "Room not found", + }), + ); + } + } catch (error) { + sshLogger.error("Failed to subscribe to collab room", error, { + operation: "collab_subscribe_error", + userId, + }); + } + break; + } + + case "collab_unsubscribe": { + const { roomId } = (data ?? {}) as { roomId?: string }; + void deps + .getSharing() + ?.unsubscribeRoom( + ws, + typeof roomId === "string" ? roomId : undefined, + ) + .catch(() => {}); + break; + } + + case "joinSharedSession": { + const joinData = data as { + shareId: string; + tabInstanceId?: string; + }; + try { + const sharing = deps.getSharing(); + const authorized = sharing + ? await sharing.authorizeJoin(joinData.shareId) + : null; + if (!sharing || !authorized) { + ws.send( + JSON.stringify({ + type: "error", + message: "Share not found or not accessible", + }), + ); + break; + } + const { share, displayName } = authorized; + + const joinedSession = sessionManager.joinAsParticipant( + share.sessionId, + ws, + { + userId, + permissionLevel: share.permissionLevel, + displayName, + tabInstanceId: joinData.tabInstanceId, + shareId: share.id, + }, + ); + if (!joinedSession) { + ws.send( + JSON.stringify({ + type: "error", + message: "Shared session is no longer active", + }), + ); + break; + } + + currentSessionId = share.sessionId; + sshStream = joinedSession.sshStream; + sshConn = joinedSession.sshConn; + isConnecting = false; + isConnected = true; + + sharing.recordJoin(share.id).catch(() => {}); + + const buffered = sessionManager.getBuffer(joinedSession); + if (buffered) { + ws.send(JSON.stringify({ type: "data", data: buffered })); + } + ws.send( + JSON.stringify({ + type: "sessionAttached", + sessionId: share.sessionId, + }), + ); + ws.send( + JSON.stringify({ + type: "connected", + message: "Joined session", + }), + ); + } catch (error) { + sshLogger.error("Failed to join shared session", error, { + operation: "terminal_join_shared_session_error", + userId, + shareId: joinData.shareId, + }); + ws.send( + JSON.stringify({ + type: "error", + message: "Failed to join shared session", + }), + ); + } + break; + } + + default: + sshLogger.warn("Unknown message type received", { + operation: "websocket_message_unknown_type", + userId, + messageType: type, + }); + } + } catch (error) { + // A malformed payload must never escape into the process-level + // unhandledRejection handler, which exits the server. + sshLogger.error("Error handling WebSocket message", error, { + operation: "websocket_message_handler_error", + userId, + messageType: type, + }); + if (ws.readyState === WebSocket.OPEN) { + ws.send( + JSON.stringify({ + type: "error", + message: "Failed to process message", + }), + ); + } + } + }), + ); + + /** + * Starts the browser sign-in behind an "<interaction>_auth_required" + * message. The provider for the host's auth type wins; otherwise the first + * provider that owns this interaction (Vault attaches to any auth type). + */ + async function startAuthInteraction( + interaction: string, + hostId: number, + payload: unknown, + ): Promise<void> { + await ctx.ssh.startInteraction(interaction, { + hostId, + socket: ws, + requestOrigin: connection.requestOrigin, + payload: (payload ?? {}) as Record<string, unknown>, + }); + } + + async function handleConnectToHost(data: ConnectToHostData) { + const { hostConfig, initialPath, executeCommand, tmuxAttachSession } = + data; + const { + id, + syncId: hostSyncId, + ip: rawIp, + port: clientPort, + username: clientUsername, + password, + key, + keyPassword, + keyType, + authType, + credentialId, + } = hostConfig; + const clientIp = rawIp?.replace(/^\[|\]$/g, "").trim() || rawIp; + let ip = clientIp; + let port = clientPort; + let username = clientUsername; + sshLogger.info("Resolving SSH host configuration", { + operation: "terminal_host_resolve", + sessionId, + userId, + hostId: id, + }); + + const sendLog = ( + stage: string, + level: string, + message: string, + details?: Record<string, unknown>, + ) => { + ws.send( + JSON.stringify({ + type: "connection_log", + data: { stage, level, message, details }, + }), + ); + }; + + if (!username || typeof username !== "string" || username.trim() === "") { + sshLogger.error("Invalid username provided", undefined, { + operation: "ssh_connect", + hostId: id, + ip, + }); + ws.send( + JSON.stringify({ + type: "error", + message: "Invalid username provided", + }), + ); + return; + } + + if (!ip || typeof ip !== "string" || ip.trim() === "") { + sshLogger.error("Invalid IP provided", undefined, { + operation: "ssh_connect", + hostId: id, + username, + }); + ws.send( + JSON.stringify({ type: "error", message: "Invalid IP provided" }), + ); + return; + } + + if (!port || typeof port !== "number" || port <= 0) { + sshLogger.error("Invalid port provided", undefined, { + operation: "ssh_connect", + hostId: id, + ip, + username, + port, + }); + ws.send( + JSON.stringify({ type: "error", message: "Invalid port provided" }), + ); + return; + } + + if (isConnecting || isConnected) { + sshLogger.warn("Connection already in progress or established", { + operation: "ssh_connect", + hostId: id, + isConnecting, + isConnected, + }); + ws.send( + JSON.stringify({ + type: "error", + message: "Connection already in progress", + code: "DUPLICATE_CONNECTION", + }), + ); + return; + } + + isConnecting = true; + sshConn = new Client(); + + sendLog("dns", "info", `Starting address resolution of ${ip}`); + sendLog("tcp", "info", `Connecting to ${ip} port ${port}`); + + const onConnectionTimeout = () => { + if (sshConn && isConnecting && !isConnected) { + sshLogger.error("SSH connection timeout", undefined, { + operation: "ssh_connect", + hostId: id, + ip, + port, + username, + }); + ws.send( + JSON.stringify({ + type: "error", + message: "SSH connection timeout", + }), + ); + if (currentSessionId) { + sessionManager.destroySession(currentSessionId); + currentSessionId = null; + } + cleanupAuthState(connectionTimeout); + } + }; + + // Reassigned when an auth provider's onBanner holds the connection open + // (Tailscale SSH check mode), so the short connect timeout does not tear + // down a connection the server is deliberately waiting on. + let connectionTimeout = setTimeout(onConnectionTimeout, 120000); + + let bannerHoldPending = false; + let authRetries = 0; + let isAuthRetrying = false; + let prepared: PluginSshPrepared | null = null; + let connectTarget: PluginSshHost | null = null; + let clearOnlineStatus: (() => void) | null = null; + + let resolvedHostData: + | (Record<string, unknown> & { + id?: number; + ip?: string; + port?: number; + username?: string; + password?: string; + key?: string; + keyPassword?: string; + keyType?: string; + authType?: string; + jumpHosts?: Array<{ hostId: number }>; + useSocks5?: boolean; + socks5Host?: string; + socks5Port?: number; + socks5Username?: string; + socks5Password?: string; + socks5ProxyChain?: unknown; + portKnockSequence?: ConnectToHostData["hostConfig"]["portKnockSequence"]; + terminalConfig?: ConnectToHostData["hostConfig"]["terminalConfig"]; + enableSessionLogging?: boolean; + }) + | null = null; + + if (id && userId) { + try { + // Prefer the sync identity. A numeric id belongs to whichever database + // produced it, so on a sync server it names a different host than the + // desktop app meant; syncId is the same string on both sides. + resolvedHostData = (await ctx.ssh.resolveHost(id, { + syncId: hostSyncId, + })) as unknown as typeof resolvedHostData | null; + + if (hostSyncId && !resolvedHostData) { + sshLogger.error( + "Refusing to connect: host is not known to this server", + undefined, + { + operation: "ssh_connect_host_sync_id_unknown", + hostId: id, + userId, + }, + ); + ws.send( + JSON.stringify({ + type: "error", + message: HOST_NOT_ON_THIS_SERVER_MESSAGE, + }), + ); + cleanupAuthState(connectionTimeout); + return; + } + + // Older clients send only the numeric id, which cannot be trusted to + // mean the same host here. Everything below is taken from the row it + // lands on -- the address, the credentials, the jump hosts, the stored + // host key -- so compare the address before using any of it. + if ( + !hostSyncId && + hostAddressMismatch(clientIp, resolvedHostData?.ip) + ) { + sshLogger.error( + "Refusing to connect: host id resolves to a different address here", + undefined, + { + operation: "ssh_connect_host_id_mismatch", + hostId: id, + userId, + clientIp, + resolvedIp: resolvedHostData?.ip, + }, + ); + ws.send( + JSON.stringify({ + type: "error", + message: HOST_ADDRESS_MISMATCH_MESSAGE, + }), + ); + cleanupAuthState(connectionTimeout); + return; + } + + if (resolvedHostData) { + const resolvedJumpHosts = resolveServerJumpHosts( + hostConfig.jumpHosts, + resolvedHostData.jumpHosts, + hostSyncId, + ); + if (resolvedJumpHosts !== hostConfig.jumpHosts) { + hostConfig.jumpHosts = resolvedJumpHosts; + sendLog( + "jump", + "info", + `Loaded ${resolvedJumpHosts?.length ?? 0} jump host(s) from server-side host data`, + ); + } + + if (resolvedHostData.useSocks5) { + hostConfig.useSocks5 = resolvedHostData.useSocks5; + hostConfig.socks5Host = resolvedHostData.socks5Host; + hostConfig.socks5Port = resolvedHostData.socks5Port; + hostConfig.socks5Username = resolvedHostData.socks5Username; + hostConfig.socks5Password = resolvedHostData.socks5Password; + hostConfig.socks5ProxyChain = resolvedHostData.socks5ProxyChain; + } + + if (!hostConfig.terminalConfig && resolvedHostData.terminalConfig) { + hostConfig.terminalConfig = resolvedHostData.terminalConfig; + } + + if ( + (!hostConfig.portKnockSequence || + hostConfig.portKnockSequence.length === 0) && + resolvedHostData.portKnockSequence && + resolvedHostData.portKnockSequence.length > 0 + ) { + hostConfig.portKnockSequence = resolvedHostData.portKnockSequence; + sendLog( + "port_knock", + "info", + `Loaded ${resolvedHostData.portKnockSequence.length} port knock(s) from server-side host data`, + ); + } + } + } catch (error) { + sshLogger.warn(`Failed to resolve server-side host data for ${id}`, { + operation: "ssh_host_data", + hostId: id, + error: getErrorMessage(error), + }); + } + } + + const serverHostId = resolveServerHostId(id, resolvedHostData); + + // Resolve credentials server-side when frontend doesn't provide them + let resolvedCredentials = { + username, + password, + key, + keyPassword, + keyType, + authType, + certPublicKey: undefined as string | undefined, + }; + const authMethodNotAvailable = false; + if (id && userId && !password && !key) { + try { + if (resolvedHostData) { + ip = resolvedHostData.ip || ip; + port = resolvedHostData.port || port; + username = resolvedHostData.username || username; + resolvedCredentials = { + username: resolvedHostData.username || username, + password: resolvedHostData.password, + key: resolvedHostData.key, + keyPassword: keyPassword || resolvedHostData.keyPassword, + keyType: resolvedHostData.keyType, + authType: resolvedHostData.authType, + certPublicKey: resolvedHostData.certPublicKey as + string | undefined, + }; + sendLog( + "auth", + "info", + "Credentials resolved from server-side host data", + ); + } + } catch (error) { + sshLogger.warn(`Failed to resolve host credentials for ${id}`, { + operation: "ssh_credentials", + hostId: id, + error: getErrorMessage(error), + }); + } + } else if (credentialId && id && userId) { + try { + if (resolvedHostData) { + ip = resolvedHostData.ip || ip; + port = resolvedHostData.port || port; + username = resolvedHostData.username || username; + resolvedCredentials = { + username: resolvedHostData.username || username, + password: resolvedHostData.password, + key: resolvedHostData.key, + // Preserve user-supplied keyPassword (e.g. from passphrase dialog) over the empty DB value + keyPassword: keyPassword || resolvedHostData.keyPassword, + keyType: resolvedHostData.keyType, + authType: resolvedHostData.authType, + certPublicKey: resolvedHostData.certPublicKey as + string | undefined, + }; + } + } catch (error) { + sshLogger.warn(`Failed to resolve credentials for host ${id}`, { + operation: "ssh_credentials", + hostId: id, + credentialId, + error: getErrorMessage(error), + }); + } + } + + if (hostConfig.passwordFallbackOnly && resolvedCredentials.password) { + resolvedCredentials = { + ...resolvedCredentials, + key: undefined, + keyPassword: undefined, + keyType: undefined, + certPublicKey: undefined, + authType: "password", + }; + } + + const connectsViaJumpHosts = !!( + hostConfig.jumpHosts && + hostConfig.jumpHosts.length > 0 && + hostConfig.userId + ); + + let connectHost = ip; + if (connectsViaJumpHosts) { + // The target is only reachable through the jump host's network (e.g. a + // VPN-only address), so DNS must be resolved there, not on this host. + sendLog( + "dns", + "info", + `Skipping local address resolution of ${ip} (resolved by jump host)`, + ); + } else { + sendLog("dns", "info", `Starting address resolution of ${ip}`); + try { + const resolution = await resolveHostForSshConnect(ip); + connectHost = resolution.host; + if (resolution.resolvedAddress && resolution.resolvedAddress !== ip) { + sendLog( + "dns", + "success", + `Resolved ${ip} to ${resolution.resolvedAddress}`, + { attempts: resolution.attempts }, + ); + } + } catch (error) { + const message = getErrorMessage(error); + sshLogger.error("SSH hostname resolution failed", error, { + operation: "terminal_dns_resolve", + hostId: id, + ip, + port, + transient: isRetriableDnsError(error), + }); + sendLog( + "dns", + "error", + `DNS resolution failed for ${ip}: ${message}`, + ); + ws.send( + JSON.stringify({ + type: "error", + message: isRetriableDnsError(error) + ? "SSH error: DNS lookup temporarily failed. Check the Docker/container DNS configuration or try again." + : "SSH error: Could not resolve hostname from the Termix server container.", + }), + ); + cleanupAuthState(connectionTimeout); + return; + } + } + sendLog("tcp", "info", `Connecting to ${ip} port ${port}`); + + // A provider can hold the handshake open pending an out-of-band step + // (Tailscale SSH check mode delivers its re-auth URL as an auth banner, + // then blocks while the user logs in via the browser). + sshConn.on( + "banner", + bind((banner: string) => { + if (!prepared?.onBanner) return; + const decision = prepared.onBanner(banner); + if (!decision) return; + + const authType = prepared.authType; + + if (decision.action === "hold") { + bannerHoldPending = true; + clearTimeout(connectionTimeout); + connectionTimeout = setTimeout( + onConnectionTimeout, + decision.timeoutMs, + ); + sendLog("auth", "info", decision.message); + ws.send( + JSON.stringify({ + type: `${authType}_check_required`, + hostId: id, + ...decision.details, + }), + ); + return; + } + + if (bannerHoldPending) { + bannerHoldPending = false; + sendLog("auth", "info", `${authType} check completed`); + ws.send( + JSON.stringify({ + type: `${authType}_check_completed`, + hostId: id, + ...decision.details, + }), + ); + } + }), + ); + + sshConn.on( + "ready", + bind(() => { + if (serverHostId != null) { + clearOnlineStatus ??= ctx.hosts.trackSession(serverHostId); + } + clearTimeout(connectionTimeout); + isAuthRetrying = false; + if (bannerHoldPending) { + bannerHoldPending = false; + ws.send( + JSON.stringify({ + type: `${prepared?.authType}_check_completed`, + hostId: id, + }), + ); + } + sshLogger.success("SSH connection established", { + operation: "terminal_ssh_connected", + sessionId, + userId, + hostId: id, + ip, + }); + + void ctx.audit.record({ + action: "ssh_connect", + resourceType: "host", + resourceId: String(id), + resourceName: `${username}@${ip}:${port}`, + success: true, + }); + if (totpPromptSent) { + authLogger.success("TOTP verification successful for SSH session", { + operation: "terminal_totp_success", + sessionId, + userId, + hostId: id, + }); + } + sendLog("handshake", "success", "SSH handshake completed"); + sendLog( + "auth", + "success", + `Authentication successful for ${username}`, + ); + sendLog("connected", "success", "Connection established"); + + const hostDisplayName = `${username}@${ip}:${port}`; + const tabInstanceId = hostConfig.instanceId; + const sessionLoggingEnabled = + resolvedHostData?.enableSessionLogging ?? + hostConfig.enableSessionLogging ?? + true; + currentSessionId = sessionManager.createSession( + userId, + id, + hostDisplayName, + data.cols, + data.rows, + tabInstanceId, + sessionLoggingEnabled, + ); + + // If createSession returned an existing live session (duplicate tabInstanceId), + // close the newly-established SSH connection and attach this WS to the live session instead. + const existingSession = sessionManager.getSession(currentSessionId); + if ( + existingSession && + existingSession.sshStream && + !existingSession.sshStream.destroyed && + existingSession.sshConn !== sshConn + ) { + const reusedSessionId = currentSessionId; + sshLogger.info( + "Reusing existing live session after duplicate connectToHost, closing new SSH conn", + { + operation: "terminal_reuse_existing_session", + sessionId: reusedSessionId, + tabInstanceId, + userId, + }, + ); + // Null out currentSessionId before ending the duplicate connection so + // the sshConn "close" handler does not destroy the reused session. + // Set isDuplicateConnDiscarded so the close handler does not send a + // "disconnected" message to the new WS that is now attached to the live session. + // Null out currentSessionId before ending the duplicate connection so + // the sshConn "close" handler does not destroy the reused session. + // Set isDuplicateConnDiscarded so the close handler exits without + // sending a "disconnected" message to the new WS. + currentSessionId = null; + isDuplicateConnDiscarded = true; + clearTimeout(connectionTimeout); + try { + sshConn?.end(); + } catch { + /* ignore */ + } + sshConn = null; + sshStream = null; + + // Point this WS handler's closure at the live session so the input + // handler can forward keystrokes via currentSessionId. + currentSessionId = reusedSessionId; + sshStream = existingSession.sshStream; + sshConn = existingSession.sshConn; + isConnecting = false; + isConnected = true; + sessionManager.attachWs(reusedSessionId, userId, ws, tabInstanceId); + + const buffered = sessionManager.getBuffer(existingSession); + if (buffered) { + ws.send(JSON.stringify({ type: "data", data: buffered })); + } + ws.send( + JSON.stringify({ + type: "sessionCreated", + sessionId: reusedSessionId, + }), + ); + ws.send( + JSON.stringify({ + type: "sessionAttached", + sessionId: reusedSessionId, + }), + ); + ws.send( + JSON.stringify({ + type: "connected", + message: "Session reattached", + }), + ); + return; + } + + sshLogger.info("Terminal session created after SSH ready", { + operation: "terminal_session_created", + sessionId: currentSessionId, + userId, + hostId: id, + tabInstanceId, + ip, + port, + }); + + const conn = sshConn; + + if (!conn || isCleaningUp || !sshConn) { + sshLogger.warn( + "SSH connection was cleaned up before shell could be created", + { + operation: "ssh_shell", + hostId: id, + ip, + port, + username, + isCleaningUp, + connNull: !conn, + sshConnNull: !sshConn, + }, + ); + ws.send( + JSON.stringify({ + type: "error", + message: + "SSH connection was closed before terminal could be created", + }), + ); + if (currentSessionId) { + sessionManager.destroySession(currentSessionId); + currentSessionId = null; + } + cleanupAuthState(connectionTimeout); + return; + } + + isShellInitializing = true; + isConnecting = false; + isConnected = true; + + if (!sshConn) { + sshLogger.error( + "SSH connection became null right before shell creation", + { + operation: "ssh_shell", + hostId: id, + }, + ); + ws.send( + JSON.stringify({ + type: "error", + message: "SSH connection lost during setup", + }), + ); + isShellInitializing = false; + if (currentSessionId) { + sessionManager.destroySession(currentSessionId); + currentSessionId = null; + } + cleanupAuthState(connectionTimeout); + return; + } + + sshLogger.info("Creating shell", { + operation: "ssh_shell_start", + hostId: id, + ip, + port, + username, + }); + + let shellCallbackReceived = false; + const shellTimeout = setTimeout(() => { + if (!shellCallbackReceived && isShellInitializing) { + sshLogger.error( + "Shell creation timeout - no response from server", + { + operation: "ssh_shell_timeout", + hostId: id, + ip, + port, + username, + }, + ); + isShellInitializing = false; + ws.send( + JSON.stringify({ + type: "error", + message: + "Shell creation timeout. The server may not support interactive shells or the connection was interrupted.", + }), + ); + if (currentSessionId) { + sessionManager.destroySession(currentSessionId); + currentSessionId = null; + } + cleanupAuthState(connectionTimeout); + } + }, 15000); + + conn.shell( + { + rows: data.rows, + cols: data.cols, + term: "xterm-256color", + } as PseudoTtyOptions, + bind((err: Error | undefined, stream: ClientChannel) => { + shellCallbackReceived = true; + clearTimeout(shellTimeout); + isShellInitializing = false; + + if (err) { + sshLogger.error("Shell error", err, { + operation: "ssh_shell", + hostId: id, + ip, + port, + username, + }); + ws.send( + JSON.stringify({ + type: "error", + message: "Shell error: " + err.message, + }), + ); + if (currentSessionId) { + sessionManager.destroySession(currentSessionId); + currentSessionId = null; + } + cleanupAuthState(connectionTimeout); + return; + } + + sshStream = stream; + sshLogger.success("Terminal shell channel opened", { + operation: "terminal_shell_opened", + sessionId, + userId, + hostId: id, + termType: "xterm-256color", + }); + + if (currentSessionId) { + sessionManager.setSSHState( + currentSessionId, + sshConn!, + stream, + lastJumpClient, + ); + sessionManager.attachWs(currentSessionId, userId, ws); + + ws.send( + JSON.stringify({ + type: "sessionCreated", + sessionId: currentSessionId, + }), + ); + + sshLogger.info("Session ready for persistence", { + operation: "session_ready", + sessionId: currentSessionId, + userId, + hostId: id, + }); + } + + const boundSessionId = currentSessionId; + // A single TCP/SSH packet boundary can split a multi-byte UTF-8 + // character (e.g. the box-drawing glyphs mc/htop use for borders). + // Buffer.toString("utf-8") on each chunk independently replaces the + // split bytes with U+FFFD, which shows up as corrupted/inserted + // characters. StringDecoder carries incomplete trailing bytes over + // to the next chunk so multi-byte characters decode correctly. + const decoder = new StringDecoder("utf-8"); + + stream.on("data", (data: Buffer) => { + try { + const utf8String = decoder.write(data); + + if (!utf8String) return; + + const session = sessionManager.getSession(boundSessionId); + if (session) { + sessionManager.bufferOutput(boundSessionId!, utf8String); + sessionManager.broadcast(boundSessionId!, { + type: "data", + data: utf8String, + }); + } + } catch (error) { + sshLogger.error("Error encoding terminal data", error, { + operation: "terminal_data_encoding", + hostId: id, + dataLength: data.length, + }); + const fallback = data.toString("latin1"); + const session = sessionManager.getSession(boundSessionId); + if (session) { + sessionManager.bufferOutput(boundSessionId!, fallback); + sessionManager.broadcast(boundSessionId!, { + type: "data", + data: fallback, + }); + } + } + }); + + stream.on("close", (code: number | null) => { + const session = sessionManager.getSession(boundSessionId); + if (session) { + if (code != null) { + sessionManager.broadcast(boundSessionId!, { + type: "session_ended", + code, + }); + } else { + sessionManager.broadcast(boundSessionId!, { + type: "disconnected", + message: "Connection lost", + graceful: true, + }); + } + } + if (boundSessionId) { + sessionManager.destroySession(boundSessionId); + if (currentSessionId === boundSessionId) { + currentSessionId = null; + } + } + }); + + stream.on("error", (err: Error) => { + sshLogger.error("SSH stream error", err, { + operation: "ssh_stream", + hostId: id, + ip, + port, + username, + }); + const session = sessionManager.getSession(boundSessionId); + if (session) { + sessionManager.broadcast(boundSessionId!, { + type: "error", + message: "SSH stream error: " + err.message, + }); + } + }); + + const autoTmux = hostConfig.terminalConfig?.autoTmux === true; + + // Helper to run initialPath/executeCommand after the shell + // (or tmux session) is ready + const runPostShellCommands = (delay: number) => { + setTimeout(() => { + if (initialPath && initialPath.trim() !== "") { + let cdCommand: string; + if (isWindowsSftpPath(initialPath)) { + const winPath = sftpPathToLocalPath(initialPath); + const escaped = winPath.replace(/"/g, '""'); + cdCommand = `cd "${escaped}"\r`; + } else { + cdCommand = `cd "${initialPath.replace(/"/g, '\\"')}"\r`; + } + stream.write(cdCommand); + } + if (executeCommand && executeCommand.trim() !== "") { + setTimeout(() => { + stream.write(`${executeCommand}\r`); + }, 300); + } + }, delay); + }; + + const tmux = deps.getTmux(); + if (tmuxAttachSession && conn && tmux) { + // Direct attach (tmux monitor): the session is known to exist, so + // skip detection and reuse the same path as the manual + // "tmux_attach" websocket message. + void tmux + .attachOrCreate(stream, tmuxAttachSession) + .catch(() => {}); + { + const session = sessionManager.getSession(boundSessionId); + if (session) session.tmuxSessionName = tmuxAttachSession; + } + sshLogger.info("Attached to requested tmux session", { + operation: "tmux_direct_attach", + sessionName: tmuxAttachSession, + hostId: id, + }); + ws.send( + JSON.stringify({ + type: "tmux_session_attached", + sessionName: tmuxAttachSession, + }), + ); + } else if (autoTmux && conn && tmux) { + (async () => { + try { + const detection = await tmux.detect(conn); + if (!detection.available) { + sshLogger.warn("tmux not found on remote host", { + operation: "tmux_detection", + hostId: id, + }); + ws.send( + JSON.stringify({ + type: "tmux_unavailable", + message: + "tmux is not installed on the remote host. Falling back to standard shell.", + }), + ); + runPostShellCommands(0); + } else if (detection.sessions.length === 0) { + const newName = `termix-${id}-${Date.now().toString(36).slice(-4)}`; + void tmux + .attachOrCreate(stream, undefined, newName) + .catch(() => {}); + const confirmed = await tmux.waitForSession( + conn, + newName, + ); + const session = sessionManager.getSession(boundSessionId); + if (session) { + session.tmuxSessionName = confirmed; + } + sshLogger.info("Created new tmux session", { + operation: "tmux_new_session", + sessionName: confirmed, + hostId: id, + }); + ws.send( + JSON.stringify({ + type: "tmux_session_created", + sessionName: confirmed, + }), + ); + runPostShellCommands(0); + } else { + sshLogger.info( + "Multiple tmux sessions found, sending list to frontend", + { + operation: "tmux_sessions_available", + sessions: detection.sessions, + hostId: id, + }, + ); + ws.send( + JSON.stringify({ + type: "tmux_sessions_available", + sessions: detection.sessions, + }), + ); + // Commands deferred until user picks a session + } + } catch (error) { + sshLogger.error("tmux detection failed", error, { + operation: "tmux_detection_error", + hostId: id, + }); + // Fallback: run commands in plain shell + runPostShellCommands(0); + } + })(); + } else { + // No tmux -- run commands directly as before + runPostShellCommands(0); + } + + ws.send( + JSON.stringify({ type: "connected", message: "SSH connected" }), + ); + + if (id && hostConfig.userId) { + try { + ctx.events.emit("host.login", { + hostId: id, + userId: hostConfig.userId, + sshUser: username, + fromIp: connection.clientIp, + }); + } catch (error) { + sshLogger.warn("Failed to dispatch login event", { + operation: "login_event_dispatch_error", + hostId: id, + error: getErrorMessage(error), + }); + } + + const hostName = + resolvedHostData?.userId === hostConfig.userId && + typeof resolvedHostData?.name === "string" && + resolvedHostData.name + ? resolvedHostData.name + : `${username}@${ip}:${port}`; + ctx.hosts + .recordActivity(id, "terminal", hostName) + .catch((error) => { + sshLogger.warn("Failed to log terminal activity", { + operation: "activity_log_error", + userId: hostConfig.userId, + hostId: id, + error: getErrorMessage(error), + }); + }); + } + }), + ); + }), + ); + + sshConn.on( + "error", + bind((err: Error) => { + clearTimeout(connectionTimeout); + + sendLog("error", "error", `Connection error: ${err.message}`); + + sshLogger.error("SSH connection error", err, { + operation: "ssh_connect", + hostId: id, + ip, + port, + username, + authType: resolvedCredentials.authType, + warpgateAuthPromptSent, + isKeyboardInteractive, + hasKeyboardInteractiveFinish: !!keyboardInteractiveFinish, + keyboardInteractiveResponded, + }); + + // The provider decides what an auth failure means: clear a cached + // certificate and ask for a new sign-in, retry once, or give up with + // its own message. + const authFollowUp = prepared?.onAuthFailed + ? prepared.onAuthFailed({ + error: err, + retries: authRetries, + canRetry: !connectConfig.sock && !bannerHoldPending, + methodNotAvailable: authMethodNotAvailable, + }) + : undefined; + + if (authFollowUp?.status === "interaction-required") { + if (currentSessionId) { + sessionManager.destroySession(currentSessionId); + currentSessionId = null; + } + cleanupAuthState(connectionTimeout); + sendLog("auth", "error", authFollowUp.message); + ws.send( + JSON.stringify({ + type: `${authFollowUp.interaction}_auth_required`, + hostId: id, + message: authFollowUp.message, + }), + ); + return; + } + + if ( + err.message.includes("Cannot parse privateKey") && + err.message.includes("no passphrase") + ) { + sendLog( + "auth", + "error", + "SSH key is encrypted but no passphrase was provided", + ); + isAwaitingAuthCredentials = true; + if (currentSessionId) { + sessionManager.destroySession(currentSessionId); + currentSessionId = null; + } + cleanupAuthState(connectionTimeout); + ws.send( + JSON.stringify({ + type: "passphrase_required", + message: + "The SSH key is encrypted. Please enter the passphrase to unlock it.", + }), + ); + return; + } + + if (authFollowUp?.status === "retry") { + authRetries++; + sendLog("auth", "info", authFollowUp.message); + sshLogger.info("Retrying SSH auth with provider changes", { + operation: "ssh_auth_retry", + hostId: id, + userId, + }); + + clearTimeout(connectionTimeout); + connectionTimeout = setTimeout( + onConnectionTimeout, + (connectConfig.readyTimeout as number | undefined) ?? 120000, + ); + + Object.assign(connectConfig, authFollowUp.patch); + + // ssh2's connect() ends an open socket and reconnects on close, keeping + // every listener attached, so the same client can be reused here. + isAuthRetrying = true; + sshConn.connect(connectConfig); + return; + } + + if (authFollowUp?.status === "error") { + sendLog("auth", "error", authFollowUp.message); + if (currentSessionId) { + sessionManager.destroySession(currentSessionId); + currentSessionId = null; + } + cleanupAuthState(connectionTimeout); + ws.send( + JSON.stringify({ type: "error", message: authFollowUp.message }), + ); + return; + } + + if ( + authMethodNotAvailable && + resolvedCredentials.authType === "none" && + !isKeyboardInteractive + ) { + sendLog( + "auth", + "error", + "Server does not support keyboard-interactive authentication", + ); + isAwaitingAuthCredentials = true; + if (currentSessionId) { + sessionManager.destroySession(currentSessionId); + currentSessionId = null; + } + cleanupAuthState(connectionTimeout); + ws.send( + JSON.stringify({ + type: "auth_method_not_available", + message: + "The server does not support keyboard-interactive authentication. Please provide credentials.", + }), + ); + return; + } + + if ( + resolvedCredentials.authType === "none" && + err.message.includes( + "All configured authentication methods failed", + ) && + !isKeyboardInteractive && + !keyboardInteractiveResponded + ) { + isAwaitingAuthCredentials = true; + if (currentSessionId) { + sessionManager.destroySession(currentSessionId); + currentSessionId = null; + } + cleanupAuthState(connectionTimeout); + ws.send( + JSON.stringify({ + type: "auth_method_not_available", + message: + "The server does not support keyboard-interactive authentication. Please provide credentials.", + }), + ); + return; + } + + if ( + isKeyboardInteractive && + keyboardInteractiveFinish && + err.message.includes("All configured authentication methods failed") + ) { + sshLogger.warn( + "Authentication error during keyboard-interactive - SKIPPING cleanup, waiting for user response", + { + operation: "ssh_error_during_keyboard_interactive_skip_cleanup", + hostId: id, + error: err.message, + }, + ); + resetConnectionState(); + return; + } + + sshLogger.error("Proceeding with cleanup after error", { + operation: "ssh_error_cleanup", + hostId: id, + error: err.message, + }); + + if ( + err.message.includes("authentication") || + err.message.includes("Authentication") + ) { + authLogger.error("SSH authentication failed", err, { + operation: "terminal_ssh_auth_failed", + sessionId, + userId, + hostId: id, + authType: resolvedCredentials.authType, + }); + sendLog("auth", "error", `Authentication failed: ${err.message}`); + } else { + sendLog("error", "error", `Connection failed: ${err.message}`); + } + + let errorMessage = "SSH error: " + err.message; + if (err.message.includes("No matching key exchange algorithm")) { + errorMessage = + "SSH error: No compatible key exchange algorithm found. This may be due to an older SSH server or network device."; + } else if (err.message.includes("No matching cipher")) { + errorMessage = + "SSH error: No compatible cipher found. This may be due to an older SSH server or network device."; + } else if (err.message.includes("No matching MAC")) { + errorMessage = + "SSH error: No compatible MAC algorithm found. This may be due to an older SSH server or network device."; + } else if ( + err.message.includes("ENOTFOUND") || + err.message.includes("ENOENT") + ) { + errorMessage = + "SSH error: Could not resolve hostname or connect to server."; + } else if (err.message.includes("ECONNREFUSED")) { + errorMessage = + "SSH error: Connection refused. The server may not be running or the port may be incorrect."; + } else if (err.message.includes("ENETUNREACH")) { + const isIPv6 = ip && ip.includes(":"); + errorMessage = isIPv6 + ? "SSH error: Network unreachable. IPv6 may not be available in this environment. If running in Docker, enable IPv6 in the Docker daemon and network configuration." + : "SSH error: Network unreachable. Check your network configuration and routing."; + } else if (err.message.includes("ETIMEDOUT")) { + errorMessage = + "SSH error: Connection timed out. Check your network connection and server availability."; + } else if ( + err.message.includes("ECONNRESET") || + err.message.includes("EPIPE") + ) { + errorMessage = + "SSH error: Connection was reset. This may be due to network issues or server timeout."; + } else if ( + err.message.includes("authentication failed") || + err.message.includes("Permission denied") + ) { + errorMessage = + "SSH error: Authentication failed. Please check your username and password/key."; + } + + ws.send(JSON.stringify({ type: "error", message: errorMessage })); + if (currentSessionId) { + sessionManager.destroySession(currentSessionId); + currentSessionId = null; + } + cleanupAuthState(connectionTimeout); + }), + ); + + sshConn.on( + "close", + bind(() => { + // The +password retry ends the socket before reconnecting; that close is + // part of the retry, not a disconnect. + if (isAuthRetrying) { + return; + } + + clearOnlineStatus?.(); + clearOnlineStatus = null; + + clearTimeout(connectionTimeout); + sshLogger.info("SSH connection closed", { + operation: "terminal_ssh_disconnected", + sessionId, + userId, + hostId: id, + }); + + if (isDuplicateConnDiscarded) { + cleanupAuthState(connectionTimeout); + return; + } + + if (isAwaitingAuthCredentials) { + if (currentSessionId) { + sessionManager.destroySession(currentSessionId); + currentSessionId = null; + } + cleanupAuthState(connectionTimeout); + return; + } + + if (isShellInitializing || (isConnected && !sshStream)) { + sshLogger.warn( + "SSH connection closed during shell initialization", + { + operation: "ssh_close_during_init", + hostId: id, + ip, + port, + username, + isShellInitializing, + hasStream: !!sshStream, + }, + ); + if (ws.readyState === WebSocket.OPEN) { + ws.send( + JSON.stringify({ + type: "error", + message: + "Connection closed during shell initialization. The server may have rejected the shell request.", + }), + ); + } + } else { + if (ws.readyState === WebSocket.OPEN) { + ws.send( + JSON.stringify({ + type: "disconnected", + message: "Connection closed", + }), + ); + } + } + if (currentSessionId) { + sessionManager.destroySession(currentSessionId); + currentSessionId = null; + } + cleanupAuthState(connectionTimeout); + }), + ); + + const sshAuthManager = new SSHAuthManager({ + ssh: ctx.ssh, + log: sshLogger, + userId, + ws, + hostId: id || 0, + isKeyboardInteractive, + keyboardInteractiveResponded, + keyboardInteractiveFinish, + totpPromptSent, + warpgateAuthPromptSent, + totpTimeout, + warpgateAuthTimeout, + totpAttempts: 0, + }); + + sshConn.on( + "keyboard-interactive", + bind( + ( + name: string, + instructions: string, + instructionsLang: string, + prompts: Array<{ prompt: string; echo: boolean }>, + finish: (responses: string[]) => void, + ) => { + if (connectionTimeout) { + clearTimeout(connectionTimeout); + } + + sshAuthManager.handleKeyboardInteractive( + name, + instructions, + instructionsLang, + prompts, + finish, + resolvedCredentials as unknown as Parameters< + typeof sshAuthManager.handleKeyboardInteractive + >[5], + hostConfig, + ); + + isKeyboardInteractive = + sshAuthManager.context.isKeyboardInteractive; + keyboardInteractiveResponded = + sshAuthManager.context.keyboardInteractiveResponded; + keyboardInteractiveFinish = + sshAuthManager.context.keyboardInteractiveFinish; + totpPromptSent = sshAuthManager.context.totpPromptSent; + warpgateAuthPromptSent = + sshAuthManager.context.warpgateAuthPromptSent; + totpTimeout = sshAuthManager.context.totpTimeout; + warpgateAuthTimeout = sshAuthManager.context.warpgateAuthTimeout; + }, + ), + ); + + const effectiveAuthType = + resolvedCredentials.authType || + (resolvedCredentials.key + ? "key" + : resolvedCredentials.password + ? "password" + : undefined); + if (!effectiveAuthType) { + sshLogger.error("No valid authentication method provided"); + ws.send( + JSON.stringify({ + type: "error", + message: "No valid authentication method provided", + }), + ); + return; + } + + connectTarget = { + ...(resolvedHostData ?? {}), + id: serverHostId ?? id, + ip, + port, + username, + userId: hostConfig.userId, + authType: effectiveAuthType, + password: resolvedCredentials.password, + key: resolvedCredentials.key, + keyPassword: resolvedCredentials.keyPassword, + keyType: resolvedCredentials.keyType, + certPublicKey: resolvedCredentials.certPublicKey, + forceKeyboardInteractive: hostConfig.forceKeyboardInteractive, + useWarpgate: hostConfig.useWarpgate, + terminalConfig: hostConfig.terminalConfig, + jumpHosts: hostConfig.jumpHosts, + useSocks5: hostConfig.useSocks5, + socks5Host: hostConfig.socks5Host, + socks5Port: hostConfig.socks5Port, + socks5Username: hostConfig.socks5Username, + socks5Password: hostConfig.socks5Password, + socks5ProxyChain: hostConfig.socks5ProxyChain, + portKnockSequence: hostConfig.portKnockSequence, + } as PluginSshHost; + + const built = await ctx.ssh.prepare(connectTarget, { + purpose: "terminal", + client: sshConn, + serverHostId: serverHostId ?? undefined, + hostKeySocket: ws, + interactive: true, + log: (level, message) => sendLog("auth", level, message), + }); + const connectConfig = built.config; + // DNS was resolved above, and skipped when jump hosts do it instead. + connectConfig.host = connectHost; + prepared = built; + + if (built.outcome.status !== "ready") { + const outcome = built.outcome; + if (outcome.status === "interaction-required") { + ws.send( + JSON.stringify({ + type: `${outcome.interaction}_auth_required`, + hostId: id, + }), + ); + return; + } + if ( + outcome.status === "error" && + outcome.code === "passphrase-required" + ) { + sendLog( + "auth", + "error", + "SSH key is encrypted but no passphrase was provided", + ); + isAwaitingAuthCredentials = true; + cleanupAuthState(connectionTimeout); + ws.send( + JSON.stringify({ + type: "passphrase_required", + message: outcome.message, + }), + ); + return; + } + sshLogger.error("SSH auth could not be prepared", { + operation: "terminal_auth_prepare", + hostId: id, + authType: effectiveAuthType, + error: outcome.message, + }); + sendLog("auth", "error", outcome.message); + ws.send(JSON.stringify({ type: "error", message: outcome.message })); + return; + } + + if (hostConfig.terminalConfig?.agentForwarding) { + if (connectConfig.privateKey) { + try { + const parsed = ssh2Utils.parseKey( + connectConfig.privateKey as Buffer, + connectConfig.passphrase as string | undefined, + ); + if (parsed && !(parsed instanceof Error)) { + connectConfig.agent = new MemoryAgent(parsed); + connectConfig.agentForward = true; + sendLog("auth", "info", "SSH agent forwarding enabled"); + } + } catch { + sshLogger.warn("Failed to set up agent forwarding", { + operation: "agent_forward_setup", + hostId: id, + }); + } + } else if (connectConfig.agent) { + connectConfig.agentForward = true; + sendLog( + "auth", + "info", + "SSH agent forwarding enabled (external agent)", + ); + } + } + + const proxyChain = Array.isArray(connectTarget.socks5ProxyChain) + ? connectTarget.socks5ProxyChain + : []; + const viaProxy = + !!connectTarget.useSocks5 && + (!!connectTarget.socks5Host || proxyChain.length > 0); + + let transport: Awaited<ReturnType<typeof ctx.ssh.openTransport>>; + try { + transport = await ctx.ssh.openTransport(connectTarget, connectConfig, { + // Resolved above, or deliberately left to the jump host. + resolveDns: false, + log: (level, message) => sendLog("handshake", level, message), + }); + } catch (error) { + sshLogger.error("SSH transport failed", error, { + operation: "ssh_transport", + hostId: id, + ip, + port, + }); + ws.send( + JSON.stringify({ + type: "error", + message: + error instanceof Error && error.name === "JumpHostChainError" + ? `Failed to connect through jump hosts: ${error.message}` + : getErrorMessage(error), + }), + ); + if (currentSessionId) { + sessionManager.destroySession(currentSessionId); + currentSessionId = null; + } + cleanupAuthState(connectionTimeout); + return; + } + lastJumpClient = transport.jumpClient as SSHClientType | null; + + sendLog( + "handshake", + "info", + transport.via === "jump" + ? "Starting SSH session through jump host" + + (viaProxy ? " (via proxy)" : "") + : transport.via === "proxy" + ? "Starting SSH session (via proxy)" + : "Starting SSH session", + ); + sendLog("auth", "info", `Authenticating as ${username}`); + sshLogger.info("Initiating SSH connection", { + operation: "terminal_ssh_connect_attempt", + sessionId, + userId, + hostId: id, + ip, + port, + username, + authType: resolvedCredentials.authType, + via: transport.via, + }); + sshConn.connect(connectConfig); + } + + function handleResize(data: ResizeData) { + const cols = toTerminalDimension(data?.cols); + const rows = toTerminalDimension(data?.rows); + if (!cols || !rows) return; + + const resizeStream = + sessionManager.getSession(currentSessionId)?.sshStream ?? sshStream; + if (resizeStream && resizeStream.setWindow) { + resizeStream.setWindow(rows, cols, rows, cols); + const session = sessionManager.getSession(currentSessionId); + if (session) { + session.cols = cols; + session.rows = rows; + sessionManager.bufferResize(session.id, cols, rows); + } + ws.send(JSON.stringify({ type: "resized", cols, rows })); + } + } + + function cleanupAuthState(timeoutId?: NodeJS.Timeout) { + if (timeoutId) { + clearTimeout(timeoutId); + } + + if (totpTimeout) { + clearTimeout(totpTimeout); + totpTimeout = null; + } + + if (warpgateAuthTimeout) { + clearTimeout(warpgateAuthTimeout); + warpgateAuthTimeout = null; + } + + sshStream = null; + sshConn = null; + lastJumpClient = null; + + resetConnectionState(); + isCleaningUp = false; + isAwaitingAuthCredentials = false; + } + + // Note: PTY-level keepalive (writing \x00 to the stream) was removed. + // It was causing ^@ characters to appear in terminals with echoctl enabled. + // SSH-level keepalive is configured via connectConfig (keepaliveInterval, + // keepaliveCountMax, tcpKeepAlive), which handles connection health monitoring + // without producing visible output on the terminal. + // + // See: https://github.com/Termix-SSH/Support/issues/232 + // See: https://github.com/Termix-SSH/Support/issues/309 + } + + return { handleConnection, closeAll }; +} diff --git a/plugins/ssh-terminal/src/frontend/TerminalTabWithRegistry.tsx b/plugins/ssh-terminal/src/frontend/TerminalTabWithRegistry.tsx index 098f59b3e..0e3b66507 100644 --- a/plugins/ssh-terminal/src/frontend/TerminalTabWithRegistry.tsx +++ b/plugins/ssh-terminal/src/frontend/TerminalTabWithRegistry.tsx @@ -1,21 +1,17 @@ import { useEffect, useRef } from "react"; import type { TabProps } from "@termix/plugin-sdk/frontend"; -import { TerminalTabContent } from "@/features/terminal/TerminalTabContent"; -import type { TabRenderProps } from "@/shell/tab-registry"; -import type { TerminalHandle } from "@/features/terminal/terminal-types"; +import { TerminalTabContent } from "./terminal/TerminalTabContent"; +import type { TerminalHandle } from "./terminal/terminal-types"; import { registerSession, setActiveSession } from "./session-registry"; /** - * Wraps the core terminal tab component to register its ref in the session - * registry, so terminal.sendToActive/sendToSession (used by the snippets - * plugin, among others) can reach a live session without core exposing - * terminalRef through the public tab props. + * Registers the tab's terminal handle in the session registry, so + * terminal.sendToActive/sendToSession (used by the snippets plugin, among + * others) can reach a live session. */ export function TerminalTabWithRegistry(props: TabProps) { - const tabRecord = props.tab as unknown as { - id: string; - terminalRef?: { current: TerminalHandle | null }; - }; + const tabRecord = props.tab as { id: string }; + const handle = props.handleRef as { current: TerminalHandle | null } | null; const host = props.host as | { id: string; @@ -33,7 +29,7 @@ export function TerminalTabWithRegistry(props: TabProps) { const tryRegister = () => { if (cancelled || registeredRef.current) return; - const ref = tabRecord.terminalRef?.current; + const ref = handle?.current; if (!ref) { if (attempts++ < 40) setTimeout(tryRegister, 250); return; @@ -64,5 +60,5 @@ export function TerminalTabWithRegistry(props: TabProps) { if (props.isFocusedPane) setActiveSession(tabRecord.id); }, [props.isFocusedPane, tabRecord.id]); - return <TerminalTabContent {...(props as unknown as TabRenderProps)} />; + return <TerminalTabContent {...props} />; } diff --git a/plugins/ssh-terminal/src/frontend/TerminalView.tsx b/plugins/ssh-terminal/src/frontend/TerminalView.tsx new file mode 100644 index 000000000..5551c305b --- /dev/null +++ b/plugins/ssh-terminal/src/frontend/TerminalView.tsx @@ -0,0 +1,38 @@ +import { Suspense, lazy, type Ref } from "react"; +import type { TerminalHandle } from "./terminal/terminal-types"; + +const CommandHistoryProvider = lazy(() => + import("./terminal/command-history/CommandHistoryContext").then((m) => ({ + default: m.CommandHistoryProvider, + })), +); +const Terminal = lazy(() => + import("./terminal/Terminal").then((m) => ({ default: m.Terminal })), +); + +/** + * The terminal as other code embeds it, registered as "terminal.view". + * + * Props are the terminal's own (hostConfig, isVisible, title, splitScreen, + * executeCommand and the rest), plus `handleRef` for the TerminalHandle + * (connect, disconnect, sendInput, fit), since a registered component cannot + * take a React ref across the plugin boundary. + */ +export function TerminalView({ + handleRef, + ...props +}: { + handleRef?: Ref<TerminalHandle>; + [prop: string]: unknown; +}) { + return ( + <Suspense fallback={null}> + <CommandHistoryProvider> + <Terminal + ref={handleRef} + {...(props as unknown as React.ComponentProps<typeof Terminal>)} + /> + </CommandHistoryProvider> + </Suspense> + ); +} diff --git a/src/ui/sidebar/HistoryPanel.tsx b/plugins/ssh-terminal/src/frontend/history/HistoryPanel.tsx similarity index 79% rename from src/ui/sidebar/HistoryPanel.tsx rename to plugins/ssh-terminal/src/frontend/history/HistoryPanel.tsx index 0f8582722..c2b62320e 100644 --- a/src/ui/sidebar/HistoryPanel.tsx +++ b/plugins/ssh-terminal/src/frontend/history/HistoryPanel.tsx @@ -1,41 +1,43 @@ import { useState, useEffect } from "react"; -import { useTranslation } from "react-i18next"; -import { copyToClipboard } from "@/lib/clipboard"; -import { Button } from "@/components/button"; -import { Input } from "@/components/input"; import { Copy, Search, Terminal, Trash2 } from "lucide-react"; +import { copyToClipboard, Button, Input } from "@termix/plugin-sdk/ui"; +import { + usePluginApi, + useTranslation, + type PanelProps, +} from "@termix/plugin-sdk/frontend"; import { - getCommandHistory, - deleteCommandFromHistory, clearCommandHistory, -} from "@/main-axios"; -import type { Tab } from "@/types/ui-types"; + deleteCommandFromHistory, + getCommandHistory, + hostSetting, +} from "../terminal-api"; -export function HistoryPanel({ - activeTabId, - terminalTabs, -}: { - activeTabId: string; - terminalTabs: Tab[]; -}) { +/** Command history for the terminal the user is working in. */ +export function HistoryPanel({ targetTab }: PanelProps) { const { t } = useTranslation(); + const api = usePluginApi(); const [search, setSearch] = useState(""); const [commands, setCommands] = useState<string[]>([]); - const activeTab = terminalTabs.find((t) => t.id === activeTabId); + const activeTab = targetTab; const activeIsTerminal = !!activeTab; const hostId = activeTab?.host?.id ? parseInt(activeTab.host.id, 10) : null; - const trackingEnabled = activeTab?.host?.enableCommandHistory !== false; + const trackingEnabled = hostSetting( + activeTab?.host, + "enableCommandHistory", + true, + ); useEffect(() => { if (!hostId || !trackingEnabled) { setCommands([]); return; } - getCommandHistory(hostId) + getCommandHistory(api, hostId) .then(setCommands) .catch(() => setCommands([])); - }, [hostId, trackingEnabled]); + }, [api, hostId, trackingEnabled]); if (activeIsTerminal && !trackingEnabled) { return ( @@ -45,10 +47,10 @@ export function HistoryPanel({ </div> <div className="flex flex-col gap-1"> <span className="text-sm font-semibold text-muted-foreground/60"> - {t("newUi.sidebar.history.trackingDisabled")} + {t("history.trackingDisabled")} </span> <span className="text-xs text-muted-foreground/40"> - {t("newUi.sidebar.history.trackingDisabledHint")} + {t("history.trackingDisabledHint")} </span> </div> </div> @@ -63,10 +65,10 @@ export function HistoryPanel({ </div> <div className="flex flex-col gap-1"> <span className="text-sm font-semibold text-muted-foreground/60"> - {t("newUi.sidebar.history.noTerminalSelected")} + {t("history.noTerminalSelected")} </span> <span className="text-xs text-muted-foreground/40"> - {t("newUi.sidebar.history.noTerminalSelectedHint")} + {t("history.noTerminalSelectedHint")} </span> </div> </div> @@ -80,7 +82,7 @@ export function HistoryPanel({ async function handleDelete(cmd: string) { if (!hostId) return; try { - await deleteCommandFromHistory(hostId, cmd); + await deleteCommandFromHistory(api, hostId, cmd); setCommands((prev) => prev.filter((c) => c !== cmd)); } catch { /* ignore */ @@ -98,7 +100,7 @@ export function HistoryPanel({ <div className="relative"> <Search className="absolute left-2.5 top-1/2 -translate-y-1/2 size-3.5 text-muted-foreground" /> <Input - placeholder={t("newUi.sidebar.history.searchPlaceholder")} + placeholder={t("history.searchPlaceholder")} value={search} onChange={(e) => setSearch(e.target.value)} className="pl-8" @@ -106,13 +108,13 @@ export function HistoryPanel({ </div> <div className="flex items-center justify-between"> <span className="text-xs text-muted-foreground"> - {filtered.length} command{filtered.length !== 1 ? "s" : ""} + {t("history.commandCount", { count: filtered.length })} </span> <button onClick={async () => { if (!hostId) return; try { - await clearCommandHistory(hostId); + await clearCommandHistory(api, hostId); } catch { /* ignore */ } @@ -120,13 +122,13 @@ export function HistoryPanel({ }} className="text-xs text-accent-brand hover:text-accent-brand/70" > - {t("newUi.sidebar.history.clearAll")} + {t("history.clearAll")} </button> </div> <div className="flex flex-col gap-1"> {filtered.length === 0 && ( <span className="text-xs text-muted-foreground/60 text-center py-8"> - {t("newUi.sidebar.history.noHistoryEntries")} + {t("history.noHistoryEntries")} </span> )} {filtered.map((cmd, i) => ( diff --git a/plugins/ssh-terminal/src/frontend/index.tsx b/plugins/ssh-terminal/src/frontend/index.tsx index f44e8346a..a27fef511 100644 --- a/plugins/ssh-terminal/src/frontend/index.tsx +++ b/plugins/ssh-terminal/src/frontend/index.tsx @@ -1,19 +1,41 @@ -import type { ComponentType } from "react"; -import { Terminal } from "lucide-react"; +import { lazy, Suspense, type ComponentType } from "react"; +import { History, Laptop, Terminal } from "lucide-react"; import type { + PanelProps, StandaloneViewProps, TabProps, TermixApp, } from "@termix/plugin-sdk/frontend"; -import { loadTerminal } from "@/features/terminal/TerminalTabContent"; -import TerminalApp from "@/features/terminal/TerminalApp"; +import { isElectron } from "@termix/plugin-sdk/ui"; +import { loadTerminal } from "./terminal/TerminalTabContent"; +import TerminalApp from "./terminal/TerminalApp"; import { - TERMINAL_DOCK_SLOT, TERMINAL_OVERLAY_SLOT, + TERMINAL_SIDE_PANEL_SLOT, TERMINAL_TOOLBAR_SLOT, -} from "@/features/terminal/terminal-slots"; + TERMINAL_TOOLBAR_STATUS_SLOT, +} from "./terminal/terminal-slots"; import { TerminalTabWithRegistry } from "./TerminalTabWithRegistry"; import { listSessions, sendToActive, sendToSession } from "./session-registry"; +import { TerminalView } from "./TerminalView"; +import { HistoryPanel } from "./history/HistoryPanel"; +import { TouchInputSettings } from "./settings/TouchInputSettings"; +import { ImageStorageTest } from "./settings/ImageStorageTest"; +import { resetTouchInputSettingsCache } from "./terminal/touch-input-settings-store"; +import { hostSetting } from "./terminal-api"; + +const LocalTerminal = lazy(() => + import("./local-terminal/LocalTerminal").then((m) => ({ + default: m.LocalTerminal, + })), +); + +interface TerminalOpenOptions { + path?: string; + joinSharedSessionId?: string; + joinShareId?: string; + label?: string; +} /** `?view=terminal` full-screen links. */ function TerminalStandalone({ hostId, params }: StandaloneViewProps) { @@ -25,9 +47,21 @@ function TerminalStandalone({ hostId, params }: StandaloneViewProps) { ); } +/** The desktop app's own shell, one tab per open. */ +function LocalTerminalTab({ tab, isVisible }: TabProps) { + return ( + <Suspense fallback={null}> + <LocalTerminal + instanceId={(tab.instanceId as string | undefined) ?? tab.id} + isVisible={isVisible} + /> + </Suspense> + ); +} + /** - * The SSH terminal. Its component still lives in core until the terminal's - * own Phase B step; this plugin decides whether the shell offers it. + * The SSH terminal: its tab, the desktop app's local terminal, the command + * history panel and the places other plugins can plug into a session. */ export function activate(app: TermixApp): void { app.registerTab( @@ -38,6 +72,8 @@ export function activate(app: TermixApp): void { titleKey: "nav.terminal", persistent: true, session: true, + commandTarget: true, + ownBackground: true, restore: (host) => !!host.enableSsh, activityTypes: ["terminal"], standalone: TerminalStandalone, @@ -54,17 +90,75 @@ export function activate(app: TermixApp): void { order: 10, tabType: "terminal", copyUrlView: "terminal", - when: (host) => !!host.enableSsh && host.enableTerminal !== false, + when: (host) => + !!host.enableSsh && hostSetting(host, "enableTerminal", true), }); - // Places other plugins can fill: toolbar buttons, a docked side panel and - // overlays that follow the session's connection flow. + if (isElectron()) { + app.registerTab("local-terminal", LocalTerminalTab, { + icon: Laptop, + titleKey: "nav.localTerminal", + hostless: true, + session: true, + ownBackground: true, + multiInstance: true, + inLayouts: false, + }); + app.registerRailItem({ + id: "local-terminal", + icon: Laptop, + titleKey: "nav.localTerminal", + kind: "tab", + electronOnly: true, + hideable: true, + separatorAfter: true, + }); + app.registerPaletteEntry({ + id: "local-terminal", + titleKey: "palette.localTerminal", + icon: Laptop, + keywords: ["local", "shell", "terminal"], + scope: "global", + run: (shell) => shell.openSingletonTab("local-terminal"), + }); + } + + app.registerPanel( + "history", + HistoryPanel as unknown as ComponentType<PanelProps>, + ); + app.registerRailItem({ + id: "history", + icon: History, + titleKey: "nav.history", + hideable: true, + promotable: true, + rightDockable: true, + separatorAfter: true, + }); + + // Places other plugins can fill: toolbar buttons and readouts, a side + // panel and overlays that follow the session's connection flow. app.declareActionSlot({ id: TERMINAL_TOOLBAR_SLOT, accepts: ["button"] }); - app.declareActionSlot({ id: TERMINAL_DOCK_SLOT, accepts: ["component"] }); + app.declareActionSlot({ + id: TERMINAL_TOOLBAR_STATUS_SLOT, + accepts: ["component"], + }); + app.declareActionSlot({ + id: TERMINAL_SIDE_PANEL_SLOT, + accepts: ["component"], + }); app.declareActionSlot({ id: TERMINAL_OVERLAY_SLOT, accepts: ["component"] }); + // A terminal other code can embed (collab rooms, the homepage widget, the + // tmux monitor, the file manager's window) without importing this plugin. + app.registerComponent( + "terminal.view", + TerminalView as unknown as ComponentType<Record<string, unknown>>, + ); + // Lets another plugin (snippets) push text into a live terminal session - // without reaching into core's tab state itself. + // without reaching into the shell's tab state itself. app.registerAction("terminal.listSessions", () => listSessions()); app.registerAction("terminal.sendToActive", (( text: string, @@ -75,4 +169,28 @@ export function activate(app: TermixApp): void { text: string, opts?: { run?: boolean }, ) => sendToSession(sessionId, text, opts)) as never); + // Opens a new terminal tab on a host: at a path (the file manager), or + // joining someone else's shared session (the connections panel). + app.registerAction("terminal.open", (( + host: Parameters<TermixApp["tabs"]["openTab"]>[0], + options: TerminalOpenOptions = {}, + ) => + app.tabs.openTab(host, "terminal", { + forceNewTab: true, + label: options.label, + data: { + ...(options.path ? { initialPath: options.path } : {}), + ...(options.joinSharedSessionId + ? { + joinSharedSessionId: options.joinSharedSessionId, + joinShareId: options.joinShareId ?? null, + } + : {}), + }, + })) as never); + + app.registerSettingsComponent("touchInput", TouchInputSettings); + app.registerSettingsComponent("imageStorageTest", ImageStorageTest); + + app.onDispose(resetTouchInputSettingsCache); } diff --git a/src/ui/lib/keybinding-dispatch.ts b/plugins/ssh-terminal/src/frontend/lib/keybinding-dispatch.ts similarity index 96% rename from src/ui/lib/keybinding-dispatch.ts rename to plugins/ssh-terminal/src/frontend/lib/keybinding-dispatch.ts index 6d7650f02..794a14361 100644 --- a/src/ui/lib/keybinding-dispatch.ts +++ b/plugins/ssh-terminal/src/frontend/lib/keybinding-dispatch.ts @@ -1,6 +1,6 @@ import type { Terminal } from "@xterm/xterm"; -import type { KeybindingAction } from "@/types/keybindings"; -import { invokeAction } from "@/shell/action-registry"; +import { type KeybindingAction } from "@termix/plugin-sdk/ui"; +import { invokeAction } from "@termix/plugin-sdk/frontend"; export interface SnippetHostContext { ip?: string; diff --git a/src/ui/lib/mac-line-navigation.ts b/plugins/ssh-terminal/src/frontend/lib/mac-line-navigation.ts similarity index 86% rename from src/ui/lib/mac-line-navigation.ts rename to plugins/ssh-terminal/src/frontend/lib/mac-line-navigation.ts index f1fd96784..91669d6e7 100644 --- a/src/ui/lib/mac-line-navigation.ts +++ b/plugins/ssh-terminal/src/frontend/lib/mac-line-navigation.ts @@ -1,4 +1,4 @@ -import { isMacPlatform } from "@/lib/tab-jump-hotkey"; +import { isMacPlatform } from "@termix/plugin-sdk/ui"; export function getMacLineNavigationSequence(e: KeyboardEvent): string | null { if (e.type !== "keydown" || !isMacPlatform()) return null; diff --git a/src/ui/lib/terminal-local-echo.ts b/plugins/ssh-terminal/src/frontend/lib/terminal-local-echo.ts similarity index 100% rename from src/ui/lib/terminal-local-echo.ts rename to plugins/ssh-terminal/src/frontend/lib/terminal-local-echo.ts diff --git a/src/ui/lib/terminal-syntax-highlighter.ts b/plugins/ssh-terminal/src/frontend/lib/terminal-syntax-highlighter.ts similarity index 100% rename from src/ui/lib/terminal-syntax-highlighter.ts rename to plugins/ssh-terminal/src/frontend/lib/terminal-syntax-highlighter.ts diff --git a/src/ui/features/local-terminal/LocalTerminal.tsx b/plugins/ssh-terminal/src/frontend/local-terminal/LocalTerminal.tsx similarity index 91% rename from src/ui/features/local-terminal/LocalTerminal.tsx rename to plugins/ssh-terminal/src/frontend/local-terminal/LocalTerminal.tsx index 685393bda..ee9403a79 100644 --- a/src/ui/features/local-terminal/LocalTerminal.tsx +++ b/plugins/ssh-terminal/src/frontend/local-terminal/LocalTerminal.tsx @@ -3,18 +3,22 @@ import { FitAddon } from "@xterm/addon-fit"; import { ClipboardAddon } from "@xterm/addon-clipboard"; import { useXTerm } from "react-xtermjs"; import { toast } from "sonner"; -import { useTranslation } from "react-i18next"; -import { useTheme } from "@/components/theme-provider"; -import { resolveTermixThemeColors } from "@/features/terminal/terminal-theme"; -import { DEFAULT_TERMINAL_CONFIG, TERMINAL_FONTS } from "@/lib/terminal-themes"; -import { getMacLineNavigationSequence } from "@/lib/mac-line-navigation"; -import { ensureTerminalFontsLoaded } from "@/features/terminal/terminal-global-styles"; +import { getMacLineNavigationSequence } from "../lib/mac-line-navigation"; import { handleTerminalClipboardKeyEvent, createTerminalContextMenuHandler, -} from "@/features/terminal/terminal-clipboard"; -import { RobustClipboardProvider } from "@/lib/clipboard-provider"; -import { copyToClipboard, readFromClipboard } from "@/lib/clipboard"; +} from "../terminal/terminal-clipboard"; +import { + useAppTheme as useTheme, + resolveTermixThemeColors, + DEFAULT_TERMINAL_CONFIG, + TERMINAL_FONTS, + ensureTerminalFontsLoaded, + RobustClipboardProvider, + copyToClipboard, + readFromClipboard, +} from "@termix/plugin-sdk/ui"; +import { useTranslation } from "@termix/plugin-sdk/frontend"; export function LocalTerminal({ instanceId, diff --git a/plugins/ssh-terminal/src/frontend/session-registry.ts b/plugins/ssh-terminal/src/frontend/session-registry.ts index 841ec9d92..29837602f 100644 --- a/plugins/ssh-terminal/src/frontend/session-registry.ts +++ b/plugins/ssh-terminal/src/frontend/session-registry.ts @@ -1,4 +1,4 @@ -import type { TerminalHandle } from "@/features/terminal/terminal-types"; +import type { TerminalHandle } from "./terminal/terminal-types"; export interface TerminalSessionInfo { id: string; diff --git a/plugins/ssh-terminal/src/frontend/settings/ImageStorageTest.tsx b/plugins/ssh-terminal/src/frontend/settings/ImageStorageTest.tsx new file mode 100644 index 000000000..7a2cb5124 --- /dev/null +++ b/plugins/ssh-terminal/src/frontend/settings/ImageStorageTest.tsx @@ -0,0 +1,100 @@ +import { useState } from "react"; +import { toast } from "sonner"; +import { Button, Input } from "@termix/plugin-sdk/ui"; +import { + usePluginApi, + useTranslation, + type SettingsComponentProps, +} from "@termix/plugin-sdk/frontend"; + +interface TestResult { + mode: string; + connected: boolean; + remoteSftpAvailable: boolean; + localHostVisible: boolean | null; + selectedMode: "local" | "remote-sftp" | "unavailable"; + localMappingConfigured: boolean; +} + +const SELECTED_MODE_LABEL_KEYS: Record<TestResult["selectedMode"], string> = { + local: "settings.admin.imageStorageMode.local", + "remote-sftp": "settings.admin.imageStorageMode.remoteSftp", + unavailable: "imageStorage.selectedModeUnavailable", +}; + +/** + * Checks which storage an image upload would use for one of the admin's own + * connected terminals, against the saved settings. + */ +export function ImageStorageTest(_props: SettingsComponentProps) { + const { t } = useTranslation(); + const api = usePluginApi(); + const [instanceId, setInstanceId] = useState(""); + const [testing, setTesting] = useState(false); + const [result, setResult] = useState<TestResult | null>(null); + + const runTest = async () => { + setTesting(true); + setResult(null); + try { + const { data } = await api.post<TestResult>("/image-storage/test", { + instanceId: instanceId.trim(), + }); + setResult(data); + } catch { + toast.error(t("imageStorage.testFailed")); + } finally { + setTesting(false); + } + }; + + return ( + <div className="flex flex-col gap-2 py-3"> + <div className="flex flex-col gap-0.5"> + <span className="text-sm font-medium leading-snug"> + {t("imageStorage.instanceId")} + </span> + <span className="text-xs text-muted-foreground leading-snug"> + {t("imageStorage.instanceIdDesc")} + </span> + </div> + <Input + aria-label={t("imageStorage.instanceId")} + value={instanceId} + onChange={(event) => setInstanceId(event.target.value)} + className="h-8 w-full @md:w-64 text-xs" + /> + {result && ( + <div className="text-xs text-muted-foreground"> + <div> + {result.connected + ? t("imageStorage.testConnected") + : t("imageStorage.testNotConnected")} + </div> + <div> + {t("imageStorage.testSelectedMode", { + mode: t(SELECTED_MODE_LABEL_KEYS[result.selectedMode]), + })} + </div> + {result.localHostVisible !== null && ( + <div> + {result.localHostVisible + ? t("imageStorage.testLocalVisible") + : t("imageStorage.testLocalNotVisible")} + </div> + )} + </div> + )} + <div> + <Button + size="sm" + variant="outline" + disabled={testing || !instanceId.trim()} + onClick={() => void runTest()} + > + {testing ? t("imageStorage.testing") : t("imageStorage.test")} + </Button> + </div> + </div> + ); +} diff --git a/plugins/ssh-terminal/src/frontend/settings/TouchInputSettings.tsx b/plugins/ssh-terminal/src/frontend/settings/TouchInputSettings.tsx new file mode 100644 index 000000000..b432e4bc2 --- /dev/null +++ b/plugins/ssh-terminal/src/frontend/settings/TouchInputSettings.tsx @@ -0,0 +1,156 @@ +import { useState } from "react"; +import { ChevronDown } from "lucide-react"; +import { + normalizeTouchInputSettings, + TOUCH_INPUT_DEFAULTS, + TOUCH_INPUT_NUMERIC_BOUNDS, + type TouchInputNumericKey, + type TouchInputSettings as TouchInputValues, +} from "../../shared/touch-input-settings"; +import { Button, Input, SettingRow, Switch } from "@termix/plugin-sdk/ui"; +import { + useTranslation, + type SettingsComponentProps, +} from "@termix/plugin-sdk/frontend"; +import { cacheTouchInputSettings } from "../terminal/touch-input-settings-store"; + +const fields: Array<{ + key: TouchInputNumericKey; + label: string; + unit: string; +}> = [ + { key: "dragThresholdPx", label: "dragThreshold", unit: "px" }, + { key: "maxWheelDeltaPx", label: "maxWheelDelta", unit: "px" }, + { + key: "momentumSampleWindowMs", + label: "momentumSampleWindow", + unit: "ms", + }, + { + key: "releaseGracePeriodMs", + label: "releaseGracePeriod", + unit: "ms", + }, + { + key: "minimumVelocityPxPerMs", + label: "minimumVelocity", + unit: "px/ms", + }, + { + key: "maximumVelocityPxPerMs", + label: "maximumVelocity", + unit: "px/ms", + }, + { key: "maximumDurationMs", label: "maximumDuration", unit: "ms" }, + { key: "maximumTravelPx", label: "maximumTravel", unit: "px" }, + { key: "decayTimeMs", label: "decayTime", unit: "ms" }, + { key: "pixelsPerTick", label: "pixelsPerTick", unit: "px" }, + { + key: "maximumFrameIntervalMs", + label: "maximumFrameInterval", + unit: "ms", + }, + { + key: "maximumTicksPerFrame", + label: "maximumTicksPerFrame", + unit: "", + }, +]; + +/** + * Touch scrolling tuning for terminals on touch screens, stored as one + * object in the "touchInput" admin setting. + */ +export function TouchInputSettings({ + values, + setValue, +}: SettingsComponentProps) { + const { t } = useTranslation(); + const [advancedOpen, setAdvancedOpen] = useState(false); + const settings = normalizeTouchInputSettings(values.touchInput); + const setSettings = (next: TouchInputValues) => { + const normalized = normalizeTouchInputSettings(next); + cacheTouchInputSettings(normalized); + setValue("touchInput", normalized); + }; + + return ( + <div className="flex flex-col gap-0"> + <SettingRow + label={t("touchInput.enabled")} + description={t("touchInput.enabledDesc")} + > + <Switch + checked={settings.enabled} + onCheckedChange={(enabled) => setSettings({ ...settings, enabled })} + /> + </SettingRow> + <SettingRow + label={t("touchInput.momentumEnabled")} + description={t("touchInput.momentumEnabledDesc")} + > + <Switch + checked={settings.momentumEnabled} + disabled={!settings.enabled} + onCheckedChange={(momentumEnabled) => + setSettings({ ...settings, momentumEnabled }) + } + /> + </SettingRow> + + <button + type="button" + className="flex items-center gap-2 py-3 text-left text-xs font-semibold text-foreground" + onClick={() => setAdvancedOpen((value) => !value)} + > + <ChevronDown + className={`size-3.5 transition-transform ${advancedOpen ? "rotate-180" : ""}`} + /> + {t("touchInput.advanced")} + </button> + {advancedOpen && ( + <div className="grid grid-cols-1 gap-3 pb-3 sm:grid-cols-2"> + {fields.map(({ key, label, unit }) => { + const bounds = TOUCH_INPUT_NUMERIC_BOUNDS[key]; + return ( + <label key={key} className="flex flex-col gap-1 text-xs"> + <span className="font-medium text-foreground"> + {t(`touchInput.${label}`)} + </span> + <div className="flex items-center gap-2"> + <Input + aria-label={t(`touchInput.${label}`)} + type="number" + min={bounds.min} + max={bounds.max} + step={bounds.step} + value={settings[key]} + onChange={(event) => + setSettings({ + ...settings, + [key]: Number(event.target.value), + }) + } + className="h-8 text-xs" + /> + {unit && ( + <span className="w-10 text-muted-foreground">{unit}</span> + )} + </div> + </label> + ); + })} + </div> + )} + <div className="flex gap-2 border-t border-border pt-3"> + <Button + size="sm" + variant="outline" + onClick={() => setSettings({ ...TOUCH_INPUT_DEFAULTS })} + > + {t("touchInput.resetDefaults")} + </Button> + </div> + </div> + ); +} diff --git a/plugins/ssh-terminal/src/frontend/terminal-api.ts b/plugins/ssh-terminal/src/frontend/terminal-api.ts new file mode 100644 index 000000000..e1b98b39b --- /dev/null +++ b/plugins/ssh-terminal/src/frontend/terminal-api.ts @@ -0,0 +1,65 @@ +import type { PluginApiClient } from "@termix/plugin-sdk/frontend"; +import { + normalizeTouchInputSettings, + type TouchInputSettings, +} from "../shared/touch-input-settings"; + +/** Calls to this plugin's own routes under /plugin-api/ssh-terminal/. */ + +export interface TerminalClientSettings { + sessionTimeoutMinutes: number; + sessionPersistence: boolean; + commandHistoryEnabled: boolean; + touchInput: TouchInputSettings; +} + +export async function getClientSettings( + api: PluginApiClient, +): Promise<TerminalClientSettings> { + const { data } = await api.get<TerminalClientSettings>("/client-settings"); + return { ...data, touchInput: normalizeTouchInputSettings(data?.touchInput) }; +} + +export async function saveCommandToHistory( + api: PluginApiClient, + hostId: number, + command: string, +): Promise<void> { + await api.post("/command-history", { hostId, command }); +} + +export async function getCommandHistory( + api: PluginApiClient, + hostId: number, +): Promise<string[]> { + const { data } = await api.get<string[]>(`/command-history/${hostId}`); + return Array.isArray(data) ? data : []; +} + +export async function deleteCommandFromHistory( + api: PluginApiClient, + hostId: number, + command: string, +): Promise<void> { + await api.post("/command-history/delete", { hostId, command }); +} + +export async function clearCommandHistory( + api: PluginApiClient, + hostId: number, +): Promise<void> { + await api.delete(`/command-history/${hostId}`); +} + +/** A host-scope setting of this plugin, as the host payload carries it. */ +export function hostSetting( + host: object | null | undefined, + key: string, + fallback: boolean, +): boolean { + const settings = ( + host as { pluginSettings?: Record<string, Record<string, unknown>> } | null + )?.pluginSettings; + const value = settings?.["ssh-terminal"]?.[key]; + return typeof value === "boolean" ? value : fallback; +} diff --git a/src/ui/features/terminal/MobileTerminalKeyboard.tsx b/plugins/ssh-terminal/src/frontend/terminal/MobileTerminalKeyboard.tsx similarity index 98% rename from src/ui/features/terminal/MobileTerminalKeyboard.tsx rename to plugins/ssh-terminal/src/frontend/terminal/MobileTerminalKeyboard.tsx index f21364e1a..45bdba85e 100644 --- a/src/ui/features/terminal/MobileTerminalKeyboard.tsx +++ b/plugins/ssh-terminal/src/frontend/terminal/MobileTerminalKeyboard.tsx @@ -1,5 +1,4 @@ import { useEffect, useRef, useState } from "react"; -import { useTranslation } from "react-i18next"; import { ChevronUp, ChevronDown, @@ -12,18 +11,19 @@ import { RotateCcw, } from "lucide-react"; import { toast } from "sonner"; -import { cn } from "@/lib/utils"; -import { Button } from "@/components/button"; -import { Input } from "@/components/input"; +import type { TerminalHandle } from "./terminal-types"; import { + cn, + Button, + Input, Sheet, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, -} from "@/components/sheet"; -import type { TerminalHandle } from "./terminal-types"; +} from "@termix/plugin-sdk/ui"; +import { useTranslation } from "@termix/plugin-sdk/frontend"; interface MobileTerminalKeyboardProps { terminalRef: React.RefObject<TerminalHandle | null>; diff --git a/src/ui/features/terminal/Terminal.tsx b/plugins/ssh-terminal/src/frontend/terminal/Terminal.tsx similarity index 96% rename from src/ui/features/terminal/Terminal.tsx rename to plugins/ssh-terminal/src/frontend/terminal/Terminal.tsx index dd2267959..71e3446a7 100644 --- a/src/ui/features/terminal/Terminal.tsx +++ b/plugins/ssh-terminal/src/frontend/terminal/Terminal.tsx @@ -3,7 +3,7 @@ import { readFontSize, saveFontSize, } from "./font-size-storage"; -import { getErrorMessage } from "../../lib/error-message.js"; +import { getErrorMessage } from "./terminal-helpers"; /* eslint-disable react-hooks/exhaustive-deps */ import { useEffect, @@ -18,89 +18,50 @@ import { createPortal } from "react-dom"; import { useXTerm } from "react-xtermjs"; import { FitAddon } from "@xterm/addon-fit"; import { ClipboardAddon } from "@xterm/addon-clipboard"; -import { RobustClipboardProvider } from "@/lib/clipboard-provider"; -import { copyToClipboard, readFromClipboard } from "@/lib/clipboard"; import { resolveTerminalContextMenuAction, selectedTextToCopy, -} from "@/features/terminal/terminal-clipboard-actions"; +} from "./terminal-clipboard-actions"; import { Unicode11Addon } from "@xterm/addon-unicode11"; import { WebLinksAddon } from "@xterm/addon-web-links"; import { SearchAddon } from "@xterm/addon-search"; -import { useTranslation } from "react-i18next"; -import { resolveConnectionOrigin } from "@/lib/connection-origin.ts"; -import { pluginWsUrl } from "@/lib/plugin-transport"; import { - getCookie, - isElectron, - logActivity, deleteCommandFromHistory, + getClientSettings, getCommandHistory, - getHostPassword, -} from "@/main-axios.ts"; -import { invokeAction } from "@/shell/action-registry"; -import { TOTPDialog } from "@/ssh/dialogs/TOTPDialog.tsx"; -import { SSHAuthDialog } from "@/ssh/dialogs/SSHAuthDialog.tsx"; -import { PassphraseDialog } from "@/ssh/dialogs/PassphraseDialog.tsx"; -import { WarpgateDialog } from "@/ssh/dialogs/WarpgateDialog.tsx"; -import { OPKSSHDialog } from "@/ssh/dialogs/OPKSSHDialog.tsx"; -import { HostKeyVerificationDialog } from "@/ssh/dialogs/HostKeyVerificationDialog.tsx"; -import { TmuxSessionPicker } from "@/ssh/dialogs/TmuxSessionPicker.tsx"; -import { - DEFAULT_TERMINAL_CONFIG, - TERMINAL_FONTS, - resolveTerminalFontFamily, -} from "@/lib/terminal-themes.ts"; -import { ensureTerminalFontsLoaded } from "./terminal-global-styles.ts"; + hostSetting, +} from "../terminal-api"; +import { TmuxSessionPicker } from "./TmuxSessionPicker"; import { getTerminalBufferText } from "./terminal-buffer-text.ts"; -import { useTheme } from "@/components/theme-provider.tsx"; -import { globalShortcutHandler } from "@/lib/global-shortcut-handler"; -import { getMacLineNavigationSequence } from "@/lib/mac-line-navigation"; -import { isTabJumpHotkey } from "@/lib/tab-jump-hotkey"; -import { useCommandTracker } from "@/features/terminal/command-history/useCommandTracker.ts"; +import { getMacLineNavigationSequence } from "../lib/mac-line-navigation"; +import { useCommandTracker } from "./command-history/useCommandTracker"; import { highlightTerminalOutput, updateControlStringMode, -} from "@/lib/terminal-syntax-highlighter.ts"; -import { useCommandHistory } from "@/features/terminal/command-history/CommandHistoryContext.tsx"; -import { getAndroidHardwareKeySequence } from "@/features/terminal/android-hardware-keyboard.ts"; +} from "../lib/terminal-syntax-highlighter"; +import { useCommandHistory } from "./command-history/CommandHistoryContext"; +import { getAndroidHardwareKeySequence } from "./android-hardware-keyboard"; import { buildImageUploadFormData, type TerminalImageUploadSource, -} from "@/features/terminal/terminal-image-upload.ts"; +} from "./terminal-image-upload"; import { TerminalSearchBar } from "./search/TerminalSearchBar.tsx"; import { CommandAutocomplete, CommandAutosuggestion, } from "./command-history/CommandAutocomplete.tsx"; -import { useConfirmation } from "@/hooks/use-confirmation.ts"; -import { useActionSlot } from "@/hooks/use-action-slot"; -import { ComponentSlot } from "@/shell/ActionSlot"; import { - TERMINAL_DOCK_SLOT, + TERMINAL_SIDE_PANEL_SLOT, TERMINAL_OVERLAY_SLOT, type TerminalOverlayProps, type TerminalSessionMessage, type TerminalSlotApi, } from "./terminal-slots"; -import { - ConnectionLogProvider, - useConnectionLog, -} from "@/ssh/connection-log/ConnectionLogContext.tsx"; -import { ConnectionScreen } from "@/components/connection/ConnectionScreen.tsx"; import { toast } from "sonner"; -import { Button } from "@/components/button"; import { Save } from "lucide-react"; -import { authApi } from "@/main-axios.ts"; -import { resolveTermixThemeColors } from "./terminal-theme.ts"; -import { ShareSessionModal } from "@/features/session-sharing/ShareSessionModal.tsx"; import { TerminalToolbar } from "./TerminalToolbar.tsx"; import type { TerminalHandle, TerminalHostConfig } from "./terminal-types.ts"; -import { type Host, type Snippet, type TabType } from "@/types/ui-types"; -import { - getNextTerminalFontSize, - getTerminalFontZoomDirection, -} from "./terminal-font-zoom.ts"; +import type { Host, Snippet, TabType } from "../types"; import { isTabKeyEvent } from "./terminal-key-event.ts"; import { installTouchWheelCoordinator } from "./touch-wheel-coordinator.ts"; import { loadTouchInputSettings } from "./touch-input-settings-store.ts"; @@ -109,24 +70,63 @@ import { getUseRightClickCopyPaste, } from "./terminal-clipboard.ts"; import { quoteTerminalImagePath } from "./terminal-image-path.ts"; -import { hydrateLocalSharedHostAuth } from "@/lib/remote-server-api.ts"; -import { - getUserPreferences, - parseCustomKeybindings, -} from "@/api/open-tabs-api"; -import { findMatchingKeybinding } from "@/lib/keybinding-match"; import { dispatchKeybindingAction, sendRawToSocket, -} from "@/lib/keybinding-dispatch"; -import { SnippetVariablesDialog } from "@/components/SnippetVariablesDialog"; -import type { CustomKeybinding } from "@/types/keybindings"; -import { useConnectionDefaults } from "@/contexts/ConnectionDefaultsContext"; +} from "../lib/keybinding-dispatch"; import { TerminalLocalEcho, resolveLocalEchoMode, -} from "@/lib/terminal-local-echo"; +} from "../lib/terminal-local-echo"; export type { TerminalHandle, TerminalHostConfig } from "./terminal-types.ts"; +import { + RobustClipboardProvider, + copyToClipboard, + readFromClipboard, + resolveConnectionOrigin, + pluginWsUrl, + TOTPDialog, + SSHAuthDialog, + PassphraseDialog, + WarpgateDialog, + OPKSSHDialog, + HostKeyVerificationDialog, + DEFAULT_TERMINAL_CONFIG, + TERMINAL_FONTS, + resolveTerminalFontFamily, + ensureTerminalFontsLoaded, + useAppTheme as useTheme, + globalShortcutHandler, + isTabJumpHotkey, + useConfirmation, + ComponentSlot, + ConnectionLogProvider, + useConnectionLog, + ConnectionScreen, + Button, + resolveTermixThemeColors, + getNextTerminalFontSize, + getTerminalFontZoomDirection, + hydrateLocalSharedHostAuth, + getUserPreferences, + parseCustomKeybindings, + findMatchingKeybinding, + SnippetVariablesDialog, + type CustomKeybinding, + useConnectionDefaults, + getCookie, + isElectron, + logActivity, + getHostPassword, + patchOpenTab, + setHostAutoTmux, +} from "@termix/plugin-sdk/ui"; +import { + useTranslation, + invokeAction, + usePluginApi, + useSlotContributions, +} from "@termix/plugin-sdk/frontend"; type HostKeyVerificationData = Omit< React.ComponentProps<typeof HostKeyVerificationDialog>, @@ -196,18 +196,19 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>( isQuickConnect = false, onSaveQuickConnect, host, - onOpenTab, isFocusedPane = true, }, ref, ) { const { t } = useTranslation(); + const api = usePluginApi(); const { instance: terminal, ref: xtermRef } = useXTerm(); const commandHistoryContext = useCommandHistory(); const { confirmWithToast } = useConfirmation(); const { theme: appTheme } = useTheme(); const { addLog } = useConnectionLog(); const { terminal: terminalDefaults } = useConnectionDefaults(); + const showToolbar = hostSetting(host, "enableTerminalToolbar", true); const outputListenersRef = useRef(new Set<(data: string) => void>()); const savedTheme = localStorage.getItem( @@ -283,7 +284,7 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>( const pongReceivedRef = useRef(true); const pongTimeoutRef = useRef<NodeJS.Timeout | null>(null); const [isConnected, setIsConnected] = useState(false); - const [shareModalOpen, setShareModalOpen] = useState(false); + const [isSavingQuickConnect, setIsSavingQuickConnect] = useState(false); const [isQuickConnectSaved, setIsQuickConnectSaved] = useState(false); const [isImageUploading, setIsImageUploading] = useState(false); @@ -475,7 +476,9 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>( id: string; props: Record<string, unknown>; } | null>(null); - const dockContributions = useActionSlot(TERMINAL_DOCK_SLOT, { host }); + const dockContributions = useSlotContributions(TERMINAL_SIDE_PANEL_SLOT, { + host, + }); const hasDock = dockContributions.length > 0; const autocompleteHistory = useRef<string[]>([]); const currentAutocompleteCommand = useRef<string>(""); @@ -525,7 +528,7 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>( if (showHistoryDialog && hostConfig.id) { setIsLoadingHistory(true); setIsLoadingRef.current(true); - getCommandHistory(hostConfig.id!) + getCommandHistory(api, hostConfig.id!) .then((history) => { setCommandHistory(history); setCommandHistoryContextRef.current(history); @@ -547,7 +550,7 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>( localStorage.getItem("commandAutocomplete") === "true"; if (hostConfig.id && autocompleteEnabled) { - getCommandHistory(hostConfig.id!) + getCommandHistory(api, hostConfig.id!) .then((history) => { autocompleteHistory.current = history; }) @@ -777,8 +780,9 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>( () => ({ host, getBufferText: () => getTerminalBufferText(terminal), - openDock: (dockId, props) => - setDock({ id: dockId, props: props ?? {} }), + sessionId: () => sessionIdRef.current, + openSidePanel: (panelId, props) => + setDock({ id: panelId, props: props ?? {} }), runCommand: handleRunCommandInTerminal, }), [host, terminal, handleRunCommandInTerminal], @@ -1419,12 +1423,19 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>( onOpenFileManager?.("/"); } }, - openShareModal: () => setShareModalOpen(true), - canShare: () => + getShareTarget: () => isConnected && !isQuickConnect && !hostConfig.joinShareId && - typeof hostConfig.id === "number", + typeof hostConfig.id === "number" && + sessionIdRef.current + ? { + hostId: hostConfig.id, + sessionId: sessionIdRef.current, + protocol: "ssh" as const, + tabInstanceId: hostConfig.instanceId, + } + : null, }), [ isConnected, @@ -1521,9 +1532,7 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>( const hostLabel = hostConfig.name || hostConfig.ip; let minutes: number | null = null; try { - const { getTerminalSessionSettings } = - await import("@/api/settings-api"); - minutes = (await getTerminalSessionSettings()).timeoutMinutes; + minutes = (await getClientSettings(api)).sessionTimeoutMinutes; } catch { /* the notice still makes sense without the number */ } @@ -1543,10 +1552,7 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>( action: { label: t("terminal.enableAutoTmuxAction"), onClick: () => { - void import("@/api/host-terminal-config-api") - .then(({ setHostAutoTmux }) => - setHostAutoTmux(hostConfig.id as number, true), - ) + void setHostAutoTmux(hostConfig.id as number, true) .then(() => { window.dispatchEvent( new CustomEvent("termix:hosts-changed"), @@ -2343,11 +2349,9 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>( sessionIdRef.current = msg.sessionId; onSessionReady?.(msg.sessionId); if (hostConfig.instanceId) { - import("@/main-axios").then(({ patchOpenTab }) => { - patchOpenTab(hostConfig.instanceId!, { - backendSessionId: msg.sessionId, - }).catch(() => {}); - }); + patchOpenTab(hostConfig.instanceId!, { + backendSessionId: msg.sessionId, + }).catch(() => {}); } } else if (msg.type === "sessionAttached") { isAttachingSessionRef.current = false; @@ -2383,11 +2387,9 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>( wasSessionExpiredRef.current = true; void explainSessionExpiry(); if (hostConfig.instanceId) { - import("@/main-axios").then(({ patchOpenTab }) => { - patchOpenTab(hostConfig.instanceId!, { - backendSessionId: null, - }).catch(() => {}); - }); + patchOpenTab(hostConfig.instanceId!, { + backendSessionId: null, + }).catch(() => {}); } if (webSocketRef.current) { webSocketRef.current.close(); @@ -2662,7 +2664,7 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>( if (!hostConfig.id) return; try { - await deleteCommandFromHistory(hostConfig.id, command); + await deleteCommandFromHistory(api, hostConfig.id, command); setCommandHistory((prev) => { const newHistory = prev.filter((cmd) => cmd !== command); @@ -2933,7 +2935,7 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>( // handling identical to desktop wheel input. let disposeTouchWheel = () => {}; let touchWheelDisposed = false; - loadTouchInputSettings().then((settings) => { + loadTouchInputSettings(api).then((settings) => { if (!touchWheelDisposed && xtermRef.current) { disposeTouchWheel = installTouchWheelCoordinator( xtermRef.current, @@ -3675,7 +3677,7 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>( hostConfig.instanceId ?? "", source, ); - const response = await authApi.post("/terminal/image-upload", form, { + const response = await api.post("/image-upload", form, { headers: { "Content-Type": undefined }, }); const { shellPath } = response.data as { @@ -3809,7 +3811,8 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>( /> {isConnected && - host?.enableTerminalToolbar === false && + host && + !showToolbar && dockContributions.map((contribution, index) => { const Icon = contribution.icon; return ( @@ -3837,7 +3840,7 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>( ); })} - {host && host.enableTerminalToolbar !== false && ( + {host && showToolbar && ( <TerminalToolbar host={host} isConnected={isConnected} @@ -3852,14 +3855,6 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>( isImageUploading={isImageUploading} onUploadImage={(file) => void handleImageUpload(file, "file")} onPasteImage={() => void handleClipboardImage()} - onOpenTab={onOpenTab} - onOpenFiles={() => { - if (webSocketRef.current?.readyState === WebSocket.OPEN) { - webSocketRef.current.send(JSON.stringify({ type: "get_cwd" })); - } else { - onOpenFileManager?.("/"); - } - }} isFocused={isFocusedPane} slotApi={slotApi} /> @@ -3877,7 +3872,7 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>( host={host} hostId={hostConfig.id} hostLabel={`${hostConfig.username}@${hostConfig.name || hostConfig.ip}`} - dockProps={dock.props} + panelProps={dock.props} onClose={closeDock} onRunInTerminal={handleRunCommandInTerminal} /> @@ -4165,7 +4160,7 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>( content: pendingKeybindingSnippet.content, folder: null, order: 0, - } as Snippet + } as Snippet as never } host={{ ip: hostConfig.ip, @@ -4320,17 +4315,6 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>( </div>, document.body, )} - - {shareModalOpen && typeof hostConfig.id === "number" && ( - <ShareSessionModal - open={shareModalOpen} - onClose={() => setShareModalOpen(false)} - hostId={hostConfig.id} - sessionId={sessionIdRef.current} - protocol="ssh" - tabInstanceId={hostConfig.instanceId} - /> - )} </div> ); }, diff --git a/src/ui/features/terminal/TerminalApp.tsx b/plugins/ssh-terminal/src/frontend/terminal/TerminalApp.tsx similarity index 86% rename from src/ui/features/terminal/TerminalApp.tsx rename to plugins/ssh-terminal/src/frontend/terminal/TerminalApp.tsx index 5585c0e2c..1555ec6fb 100644 --- a/src/ui/features/terminal/TerminalApp.tsx +++ b/plugins/ssh-terminal/src/frontend/terminal/TerminalApp.tsx @@ -1,8 +1,7 @@ import React from "react"; -import { useTranslation } from "react-i18next"; -import { Terminal } from "@/features/terminal/Terminal.tsx"; -import { FullScreenAppWrapper } from "@/features/FullScreenAppWrapper.tsx"; -import { ConnectionScreen } from "@/components/connection/ConnectionScreen.tsx"; +import { Terminal } from "./Terminal"; +import { FullScreenAppWrapper, ConnectionScreen } from "@termix/plugin-sdk/ui"; +import { useTranslation } from "@termix/plugin-sdk/frontend"; interface TerminalAppProps { hostId?: string; diff --git a/src/ui/features/terminal/TerminalTabContent.tsx b/plugins/ssh-terminal/src/frontend/terminal/TerminalTabContent.tsx similarity index 65% rename from src/ui/features/terminal/TerminalTabContent.tsx rename to plugins/ssh-terminal/src/frontend/terminal/TerminalTabContent.tsx index c740bc286..4d1873e2e 100644 --- a/src/ui/features/terminal/TerminalTabContent.tsx +++ b/plugins/ssh-terminal/src/frontend/terminal/TerminalTabContent.tsx @@ -1,12 +1,28 @@ import { lazy, Suspense } from "react"; import { TerminalSquare } from "lucide-react"; -import { useTranslation } from "react-i18next"; import type { TerminalHandle, TerminalHostConfig } from "./Terminal"; -import { useIsMobile } from "@/hooks/use-mobile"; -import { useTabsSafe } from "@/shell/TabContext"; -import { hostToSSHHost } from "@/lib/host-to-ssh-host"; -import { invokeAction } from "@/shell/action-registry"; -import type { TabRenderProps } from "@/shell/tab-registry"; +import { useIsMobile, useTabsSafe } from "@termix/plugin-sdk/ui"; +import { + useTranslation, + invokeAction, + type TabProps, +} from "@termix/plugin-sdk/frontend"; +import type { Host } from "../types"; + +/** The session fields the shell keeps on a terminal tab. */ +interface TerminalTabRecord { + id: string; + instanceId?: string; + restoredSessionId?: string | null; + joinSharedSessionId?: string | null; + joinShareId?: string | null; + initialFilePath?: string; + data?: { + initialPath?: string; + joinSharedSessionId?: string | null; + joinShareId?: string | null; + }; +} const CommandHistoryProvider = lazy(() => import("./command-history/CommandHistoryContext").then((m) => ({ @@ -30,20 +46,19 @@ function TabChunkFallback() { ); } -/** - * The SSH terminal as a tab. Core code, because the terminal's session - * manager, split view and file-manager hooks are core until the terminal's - * own Phase B step; the ssh-terminal plugin registers it as the "terminal" - * tab type, so it comes and goes with that plugin. - */ +/** The SSH terminal as a tab. */ export function TerminalTabContent({ - tab, - host, + tab: tabRecord, + host: hostRecord, + sshHost, label, isVisible, isFocusedPane, + handleRef, shell, -}: TabRenderProps) { +}: TabProps) { + const tab = tabRecord as unknown as TerminalTabRecord; + const host = hostRecord as unknown as Host | undefined; const { t } = useTranslation(); const { previewTerminalTheme } = useTabsSafe(); const isMobile = useIsMobile(); @@ -65,19 +80,22 @@ export function TerminalTabContent({ <div className="flex flex-col h-full w-full"> <div className="flex-1 min-h-0"> <TerminalFeature - ref={tab.terminalRef as React.Ref<TerminalHandle>} + ref={handleRef as React.Ref<TerminalHandle>} hostConfig={ { - ...hostToSSHHost(host), + ...sshHost, sshPort: host.sshPort ?? host.port, instanceId: tab.instanceId ?? tab.id, restoredSessionId: tab.restoredSessionId ?? null, - joinSharedSessionId: tab.joinSharedSessionId ?? null, - joinShareId: tab.joinShareId ?? null, - } as TerminalHostConfig + joinSharedSessionId: + tab.data?.joinSharedSessionId ?? + tab.joinSharedSessionId ?? + null, + joinShareId: tab.data?.joinShareId ?? tab.joinShareId ?? null, + } as unknown as TerminalHostConfig } isVisible={isVisible} - initialPath={tab.initialFilePath} + initialPath={tab.data?.initialPath ?? tab.initialFilePath} title={label} showTitle={false} splitScreen={false} @@ -94,22 +112,20 @@ export function TerminalTabContent({ onOpenFileManager={(path) => void invokeAction("files.openHost", host, path) } - isQuickConnect={host.id.startsWith("quick-connect-")} + isQuickConnect={String(host.id).startsWith("quick-connect-")} onSaveQuickConnect={ shell.saveQuickConnect - ? () => shell.saveQuickConnect!(tab, host) + ? () => shell.saveQuickConnect!(tabRecord, hostRecord!) : undefined } host={host} - onOpenTab={(type) => shell.openTab(host, type)} + onOpenTab={(type) => shell.openTab(hostRecord!, type)} isFocusedPane={isFocusedPane} /> </div> {isMobile && ( <MobileTerminalKeyboard - terminalRef={ - tab.terminalRef as React.RefObject<TerminalHandle | null> - } + terminalRef={handleRef as React.RefObject<TerminalHandle | null>} /> )} </div> diff --git a/src/ui/features/terminal/TerminalToolbar.tsx b/plugins/ssh-terminal/src/frontend/terminal/TerminalToolbar.tsx similarity index 77% rename from src/ui/features/terminal/TerminalToolbar.tsx rename to plugins/ssh-terminal/src/frontend/terminal/TerminalToolbar.tsx index 103f5f409..e3282db5c 100644 --- a/src/ui/features/terminal/TerminalToolbar.tsx +++ b/plugins/ssh-terminal/src/frontend/terminal/TerminalToolbar.tsx @@ -6,7 +6,6 @@ import React, { useRef, useState, } from "react"; -import { useTranslation } from "react-i18next"; import { ChevronDown, ClipboardPaste, @@ -18,29 +17,13 @@ import { Minimize2, X, } from "lucide-react"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, -} from "@/components/select"; -import { cn } from "@/lib/utils"; -import { TERMINAL_TOOLBAR_SLOT, type TerminalSlotApi } from "./terminal-slots"; -import { ActionSlot } from "@/shell/ActionSlot"; -import { useActionSlot } from "@/hooks/use-action-slot"; -import { useIsMobile } from "@/hooks/use-mobile"; -import { getSshActions } from "@/sidebar/tree/HostItem/HostItem"; import { - getServerMetricsById, - startMetricsPolling, - stopMetricsPolling, -} from "@/api/host-metrics-status-api"; -import type { Host, TabType } from "@/types/ui-types"; -import { - getPollingEnvironmentMultiplier, - runAdaptivePolling, -} from "@/lib/adaptive-polling"; + TERMINAL_TOOLBAR_SLOT, + TERMINAL_TOOLBAR_STATUS_SLOT, + type TerminalSlotApi, +} from "./terminal-slots"; +import type { Host } from "../types"; import { clampToolbarPosition, getResponsiveToolbarDensity, @@ -49,6 +32,20 @@ import { type ToolbarPosition, type ToolbarDensity, } from "./toolbar-geometry"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + cn, + ActionSlot, + ComponentSlot, + useIsMobile, +} from "@termix/plugin-sdk/ui"; +import { + useTranslation, + useSlotContributions, +} from "@termix/plugin-sdk/frontend"; type SelectedToolbarDensity = ToolbarDensity; const DENSITY_STORAGE_KEY = "termix-terminal-toolbar-density"; @@ -79,41 +76,6 @@ const CONTROL = "inline-flex min-h-8 min-w-8 items-center justify-center gap-1.5 rounded-sm px-2 text-xs font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-foreground active:translate-y-px active:bg-muted/80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-40 data-[state=open]:bg-muted data-[state=open]:text-foreground"; const SEPARATOR = "mx-0.5 h-5 w-px shrink-0 bg-border"; -function StatBar({ - label, - percent, -}: { - label: string; - percent: number | null; -}) { - const value = percent == null ? null : Math.max(0, Math.min(100, percent)); - return ( - <div className="flex items-center gap-1.5 px-1.5"> - <span className="w-12 shrink-0 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground"> - {label} - </span> - <div className="h-1.5 w-12 shrink-0 overflow-hidden rounded-full bg-muted"> - {value != null && ( - <div - className={cn( - "h-full rounded-full transition-all", - value >= 90 - ? "bg-red-500" - : value >= 70 - ? "bg-yellow-500" - : "bg-accent-brand", - )} - style={{ width: `${value}%` }} - /> - )} - </div> - <span className="w-7 shrink-0 text-right text-[10px] tabular-nums text-muted-foreground"> - {value == null ? "--" : `${Math.round(value)}%`} - </span> - </div> - ); -} - interface TerminalToolbarProps { host: Host; terminalIdentity?: string; @@ -123,8 +85,6 @@ interface TerminalToolbarProps { isImageUploading: boolean; onUploadImage: (file: File) => void | Promise<void>; onPasteImage: () => void | Promise<void>; - onOpenTab?: (type: TabType) => void; - onOpenFiles?: () => void; isFocused: boolean; /** Hides every contributed action, e.g. while the toolbar is measuring. */ actionsEnabled?: boolean; @@ -141,8 +101,6 @@ export const TerminalToolbar: React.FC<TerminalToolbarProps> = ({ isImageUploading, onUploadImage, onPasteImage, - onOpenTab, - onOpenFiles, isFocused, actionsEnabled = true, slotApi, @@ -150,7 +108,11 @@ export const TerminalToolbar: React.FC<TerminalToolbarProps> = ({ const { t } = useTranslation(); // Drives both the measurement copy and the separator, so the toolbar sizes // itself correctly whether or not anything contributed. - const allSlotContributions = useActionSlot(TOOLBAR_SLOT_ID, { host }); + const allSlotContributions = useSlotContributions(TOOLBAR_SLOT_ID, { host }); + const statusContributions = useSlotContributions( + TERMINAL_TOOLBAR_STATUS_SLOT, + { host }, + ); const slotContributions = useMemo( () => allSlotContributions.filter( @@ -168,12 +130,6 @@ export const TerminalToolbar: React.FC<TerminalToolbarProps> = ({ const positionRef = useRef(position); const [collapsed, setCollapsed] = useState(false); const [densityOpen, setDensityOpen] = useState(false); - const [metrics, setMetrics] = useState<{ - cpu: number | null; - memory: number | null; - disk: number | null; - } | null>(null); - const [statsAvailable, setStatsAvailable] = useState(true); const [imageError, setImageError] = useState<string | null>(null); const [retryImageAction, setRetryImageAction] = useState<(() => void) | null>( null, @@ -199,14 +155,11 @@ export const TerminalToolbar: React.FC<TerminalToolbarProps> = ({ const [desktopViewportReady, setDesktopViewportReady] = useState<boolean>(); const hostIdentity = terminalIdentity ?? `${host.id ?? "unknown"}`; positionRef.current = position; - const hostActions = getSshActions(host).filter( - (action) => action.type !== "terminal", - ); const effectiveDensity: ToolbarDensity = isFocused ? responsiveDensity : "icon"; - const showStats = effectiveDensity === "expanded"; - const hostId = host.id ? Number(host.id) : null; + const showStats = + effectiveDensity === "expanded" && statusContributions.length > 0; const isMobile = useIsMobile(); useLayoutEffect(() => { @@ -275,7 +228,6 @@ export const TerminalToolbar: React.FC<TerminalToolbarProps> = ({ }, [ density, desktopViewportReady, - hostActions.length, isConnected, isMobile, isTmuxAttached, @@ -349,7 +301,6 @@ export const TerminalToolbar: React.FC<TerminalToolbarProps> = ({ density, desktopViewportReady, effectiveDensity, - hostActions.length, isConnected, isMobile, isTmuxAttached, @@ -368,98 +319,6 @@ export const TerminalToolbar: React.FC<TerminalToolbarProps> = ({ hideToolbarRef.current?.focus(); }, [collapsed]); - useEffect(() => { - if ( - !showStats || - desktopViewportReady !== true || - isMobile !== false || - !hostId || - collapsed || - !isConnected - ) { - setMetrics(null); - return; - } - let cancelled = false; - let stopPolling: (() => void) | undefined; - let viewerSessionId: string | undefined; - let previousMetrics: typeof metrics = null; - setStatsAvailable(true); - - const poll = async () => { - try { - const data = await getServerMetricsById(hostId); - if (cancelled || !data) return; - const next = { - cpu: data.cpu?.percent ?? null, - memory: data.memory?.percent ?? null, - disk: data.disk?.percent ?? null, - }; - const changed = - !previousMetrics || - (["cpu", "memory", "disk"] as const).some((key) => { - const previous = previousMetrics?.[key]; - const current = next[key]; - return ( - previous == null || - current == null || - Math.abs(current - previous) >= 2 - ); - }); - previousMetrics = next; - setMetrics(next); - return changed; - } catch { - // Retain the previous sample after a transient polling error. - return false; - } - }; - const start = async () => { - try { - const result = await startMetricsPolling(hostId); - if (cancelled) { - if (result.viewerSessionId) { - void stopMetricsPolling(hostId, result.viewerSessionId).catch( - () => {}, - ); - } - return; - } - if (result.requires_totp) { - setStatsAvailable(false); - return; - } - viewerSessionId = result.viewerSessionId; - stopPolling = runAdaptivePolling( - poll, - { - minIntervalMs: 5_000, - maxIntervalMs: 20_000, - stablePollsPerStep: 2, - maxRequestDutyCycle: 0.2, - }, - { intervalMultiplier: getPollingEnvironmentMultiplier }, - ); - } catch { - if (!cancelled) setStatsAvailable(false); - } - }; - void start(); - return () => { - cancelled = true; - stopPolling?.(); - void stopMetricsPolling(hostId, viewerSessionId).catch(() => {}); - }; - }, [ - desktopViewportReady, - showStats, - isMobile, - hostId, - collapsed, - isConnected, - hostIdentity, - ]); - if (!isConnected || desktopViewportReady !== true || isMobile !== false) return null; @@ -601,27 +460,6 @@ export const TerminalToolbar: React.FC<TerminalToolbarProps> = ({ </Select> ); - const renderHostAction = (action: (typeof hostActions)[number]) => { - const Icon = action.icon; - const invoke = () => - action.type === "files" && onOpenFiles - ? onOpenFiles() - : onOpenTab?.(action.type); - return ( - <button - key={action.type} - type="button" - className={CONTROL} - aria-label={action.label} - title={action.label} - onClick={invoke} - > - <Icon className="size-4 shrink-0" /> - {effectiveDensity !== "icon" && action.label} - </button> - ); - }; - const imageButtons = ( <> <button @@ -744,12 +582,6 @@ export const TerminalToolbar: React.FC<TerminalToolbarProps> = ({ aria-hidden="true" className="pointer-events-none invisible absolute left-0 top-0 flex whitespace-nowrap" > - {hostActions.map((action) => ( - <span key={action.type} className={CONTROL}> - <action.icon className="size-4" /> - {density !== "icon" && action.label} - </span> - ))} {isTmuxAttached && ( <span className={CONTROL}> <LogOut className="size-4" /> @@ -802,33 +634,21 @@ export const TerminalToolbar: React.FC<TerminalToolbarProps> = ({ <div className="flex flex-col overflow-hidden rounded-sm border border-border bg-background/90 shadow-lg backdrop-blur-sm"> {showStats && ( <div className="flex flex-wrap items-center justify-center gap-x-1 border-b border-border px-1.5 py-0.5"> - {statsAvailable ? ( - <> - <StatBar - label={t("terminalToolbar.cpu")} - percent={metrics?.cpu ?? null} - /> - <StatBar - label={t("terminalToolbar.memory")} - percent={metrics?.memory ?? null} - /> - <StatBar - label={t("terminalToolbar.disk")} - percent={metrics?.disk ?? null} - /> - </> - ) : ( - <span - role="status" - className="px-2 py-1 text-xs text-muted-foreground" - > - {t("terminalToolbar.statsUnavailable")} - </span> - )} + <ComponentSlot + slotId={TERMINAL_TOOLBAR_STATUS_SLOT} + when={{ host }} + props={{ + host, + isConnected, + active: + isConnected && + desktopViewportReady === true && + isMobile === false, + }} + /> </div> )} <div className={cn("flex gap-0.5 p-0.5", "items-center")}> - {hostActions.map(renderHostAction)} {isTmuxAttached && ( <button type="button" @@ -842,9 +662,7 @@ export const TerminalToolbar: React.FC<TerminalToolbarProps> = ({ t("terminalToolbar.detachTmux")} </button> )} - {(hostActions.length > 0 || isTmuxAttached) && ( - <div className={SEPARATOR} /> - )} + {isTmuxAttached && <div className={SEPARATOR} />} <div role="group" aria-label={t("terminalToolbar.image")} diff --git a/src/ui/ssh/dialogs/TmuxSessionPicker.tsx b/plugins/ssh-terminal/src/frontend/terminal/TmuxSessionPicker.tsx similarity index 97% rename from src/ui/ssh/dialogs/TmuxSessionPicker.tsx rename to plugins/ssh-terminal/src/frontend/terminal/TmuxSessionPicker.tsx index 056f00073..95666f326 100644 --- a/src/ui/ssh/dialogs/TmuxSessionPicker.tsx +++ b/plugins/ssh-terminal/src/frontend/terminal/TmuxSessionPicker.tsx @@ -1,7 +1,7 @@ import React from "react"; -import { Button } from "@/components/button.tsx"; import { Terminal, Monitor, Users, Clock } from "lucide-react"; -import { useTranslation } from "react-i18next"; +import { Button } from "@termix/plugin-sdk/ui"; +import { useTranslation } from "@termix/plugin-sdk/frontend"; interface TmuxSessionInfo { name: string; diff --git a/src/ui/features/terminal/android-hardware-keyboard.ts b/plugins/ssh-terminal/src/frontend/terminal/android-hardware-keyboard.ts similarity index 92% rename from src/ui/features/terminal/android-hardware-keyboard.ts rename to plugins/ssh-terminal/src/frontend/terminal/android-hardware-keyboard.ts index f99e62cfc..69c7cddc9 100644 --- a/src/ui/features/terminal/android-hardware-keyboard.ts +++ b/plugins/ssh-terminal/src/frontend/terminal/android-hardware-keyboard.ts @@ -1,4 +1,4 @@ -import type { HostBackspaceMode } from "@/sidebar/HostEditorData"; +import type { HostBackspaceMode } from "../types"; const CURSOR_SEQUENCES: Record<string, [normal: string, application: string]> = { diff --git a/src/ui/features/terminal/command-history/CommandAutocomplete.tsx b/plugins/ssh-terminal/src/frontend/terminal/command-history/CommandAutocomplete.tsx similarity index 98% rename from src/ui/features/terminal/command-history/CommandAutocomplete.tsx rename to plugins/ssh-terminal/src/frontend/terminal/command-history/CommandAutocomplete.tsx index 7393e72c0..3f8578eee 100644 --- a/src/ui/features/terminal/command-history/CommandAutocomplete.tsx +++ b/plugins/ssh-terminal/src/frontend/terminal/command-history/CommandAutocomplete.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useRef } from "react"; -import { cn } from "@/lib/utils.ts"; +import { cn } from "@termix/plugin-sdk/ui"; interface CommandAutocompleteProps { suggestions: string[]; diff --git a/src/ui/features/terminal/command-history/CommandHistoryContext.tsx b/plugins/ssh-terminal/src/frontend/terminal/command-history/CommandHistoryContext.tsx similarity index 100% rename from src/ui/features/terminal/command-history/CommandHistoryContext.tsx rename to plugins/ssh-terminal/src/frontend/terminal/command-history/CommandHistoryContext.tsx diff --git a/src/ui/features/terminal/command-history/useCommandTracker.ts b/plugins/ssh-terminal/src/frontend/terminal/command-history/useCommandTracker.ts similarity index 93% rename from src/ui/features/terminal/command-history/useCommandTracker.ts rename to plugins/ssh-terminal/src/frontend/terminal/command-history/useCommandTracker.ts index eb4e64d99..7fb7005e2 100644 --- a/src/ui/features/terminal/command-history/useCommandTracker.ts +++ b/plugins/ssh-terminal/src/frontend/terminal/command-history/useCommandTracker.ts @@ -1,5 +1,6 @@ import { useRef, useCallback } from "react"; -import { saveCommandToHistory } from "@/main-axios.ts"; +import { usePluginApi } from "@termix/plugin-sdk/frontend"; +import { saveCommandToHistory } from "../../terminal-api"; const SENSITIVE_PATTERNS = [ /\bpassw(or)?d\b/i, @@ -34,6 +35,7 @@ export function useCommandTracker({ enabled = true, onCommandExecuted, }: UseCommandTrackerOptions): CommandTrackerResult { + const api = usePluginApi(); const currentCommandRef = useRef<string>(""); const isInEscapeSequenceRef = useRef<boolean>(false); @@ -70,7 +72,7 @@ export function useCommandTracker({ const isSensitive = SENSITIVE_PATTERNS.some((p) => p.test(command)); if (!isSensitive) { - saveCommandToHistory(hostId, command).catch((error) => { + saveCommandToHistory(api, hostId, command).catch((error) => { console.error("Failed to save command to history:", error); }); } diff --git a/src/ui/features/terminal/font-size-storage.ts b/plugins/ssh-terminal/src/frontend/terminal/font-size-storage.ts similarity index 100% rename from src/ui/features/terminal/font-size-storage.ts rename to plugins/ssh-terminal/src/frontend/terminal/font-size-storage.ts diff --git a/src/ui/features/terminal/search/TerminalSearchBar.tsx b/plugins/ssh-terminal/src/frontend/terminal/search/TerminalSearchBar.tsx similarity index 96% rename from src/ui/features/terminal/search/TerminalSearchBar.tsx rename to plugins/ssh-terminal/src/frontend/terminal/search/TerminalSearchBar.tsx index c30f31585..406f76767 100644 --- a/src/ui/features/terminal/search/TerminalSearchBar.tsx +++ b/plugins/ssh-terminal/src/frontend/terminal/search/TerminalSearchBar.tsx @@ -1,16 +1,16 @@ import React from "react"; import { ChevronDown, ChevronUp, X } from "lucide-react"; -import { useTranslation } from "react-i18next"; -import { Input } from "@/components/input"; -import { Button } from "@/components/button"; -import { Separator } from "@/components/separator"; import { + Input, + Button, + Separator, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, -} from "@/components/tooltip"; -import { cn } from "@/lib/utils.ts"; + cn, +} from "@termix/plugin-sdk/ui"; +import { useTranslation } from "@termix/plugin-sdk/frontend"; interface TerminalSearchBarProps { visible: boolean; diff --git a/src/ui/features/terminal/terminal-buffer-text.ts b/plugins/ssh-terminal/src/frontend/terminal/terminal-buffer-text.ts similarity index 100% rename from src/ui/features/terminal/terminal-buffer-text.ts rename to plugins/ssh-terminal/src/frontend/terminal/terminal-buffer-text.ts diff --git a/src/ui/features/terminal/terminal-clipboard-actions.ts b/plugins/ssh-terminal/src/frontend/terminal/terminal-clipboard-actions.ts similarity index 100% rename from src/ui/features/terminal/terminal-clipboard-actions.ts rename to plugins/ssh-terminal/src/frontend/terminal/terminal-clipboard-actions.ts diff --git a/src/ui/features/terminal/terminal-clipboard.ts b/plugins/ssh-terminal/src/frontend/terminal/terminal-clipboard.ts similarity index 98% rename from src/ui/features/terminal/terminal-clipboard.ts rename to plugins/ssh-terminal/src/frontend/terminal/terminal-clipboard.ts index 022391e10..a625fe4c3 100644 --- a/src/ui/features/terminal/terminal-clipboard.ts +++ b/plugins/ssh-terminal/src/frontend/terminal/terminal-clipboard.ts @@ -1,5 +1,5 @@ import type { Terminal } from "@xterm/xterm"; -import { getCookie } from "@/main-axios.ts"; +import { getCookie } from "@termix/plugin-sdk/ui"; import { isPhysicalShortcutKey } from "./terminal-key-event"; export function getUseRightClickCopyPaste(): boolean { diff --git a/plugins/ssh-terminal/src/frontend/terminal/terminal-helpers.ts b/plugins/ssh-terminal/src/frontend/terminal/terminal-helpers.ts new file mode 100644 index 000000000..6ef95453a --- /dev/null +++ b/plugins/ssh-terminal/src/frontend/terminal/terminal-helpers.ts @@ -0,0 +1,6 @@ +export function getErrorMessage( + error: unknown, + fallback = "Unknown error", +): string { + return error instanceof Error ? error.message : fallback; +} diff --git a/src/ui/features/terminal/terminal-image-path.ts b/plugins/ssh-terminal/src/frontend/terminal/terminal-image-path.ts similarity index 100% rename from src/ui/features/terminal/terminal-image-path.ts rename to plugins/ssh-terminal/src/frontend/terminal/terminal-image-path.ts diff --git a/src/ui/features/terminal/terminal-image-upload.ts b/plugins/ssh-terminal/src/frontend/terminal/terminal-image-upload.ts similarity index 100% rename from src/ui/features/terminal/terminal-image-upload.ts rename to plugins/ssh-terminal/src/frontend/terminal/terminal-image-upload.ts diff --git a/src/ui/features/terminal/terminal-key-event.ts b/plugins/ssh-terminal/src/frontend/terminal/terminal-key-event.ts similarity index 100% rename from src/ui/features/terminal/terminal-key-event.ts rename to plugins/ssh-terminal/src/frontend/terminal/terminal-key-event.ts diff --git a/src/ui/features/terminal/terminal-slots.ts b/plugins/ssh-terminal/src/frontend/terminal/terminal-slots.ts similarity index 60% rename from src/ui/features/terminal/terminal-slots.ts rename to plugins/ssh-terminal/src/frontend/terminal/terminal-slots.ts index b6329df55..6deeecc03 100644 --- a/src/ui/features/terminal/terminal-slots.ts +++ b/plugins/ssh-terminal/src/frontend/terminal/terminal-slots.ts @@ -1,11 +1,13 @@ -import type { Host } from "@/types/ui-types"; +import type { Host } from "../types"; /** * The slots the terminal offers plugins, and what it hands them. * * - "terminal.toolbar" (buttons): actions are invoked with a TerminalSlotApi. - * - "terminal.dock" (components): a side panel an action opens with - * `openDock(id)`, rendered with TerminalDockProps. + * - "terminal.toolbarStatus" (components): small live readouts at the end + * of the toolbar (host metrics), rendered with TerminalToolbarStatusProps. + * - "terminal.sidePanel" (components): a side panel an action opens with + * `openSidePanel(id)`, rendered with TerminalSidePanelProps. * - "terminal.overlay" (components): always mounted over the terminal and * given the session's server messages, for connection flows a plugin owns * (a Tailscale check, say). @@ -13,25 +15,35 @@ import type { Host } from "@/types/ui-types"; * Every contribution's `when` sees `{ host }`. */ export const TERMINAL_TOOLBAR_SLOT = "terminal.toolbar"; -export const TERMINAL_DOCK_SLOT = "terminal.dock"; +export const TERMINAL_TOOLBAR_STATUS_SLOT = "terminal.toolbarStatus"; +export const TERMINAL_SIDE_PANEL_SLOT = "terminal.sidePanel"; export const TERMINAL_OVERLAY_SLOT = "terminal.overlay"; export interface TerminalSlotApi { host: Host | undefined; /** The visible scrollback, read at call time. */ getBufferText: () => string; - /** Opens a "terminal.dock" contribution by its actionId. */ - openDock: (dockId: string, props?: Record<string, unknown>) => void; + /** The live session's id, once the server created it. */ + sessionId: () => string | null; + /** Opens a "terminal.sidePanel" contribution by its actionId. */ + openSidePanel: (panelId: string, props?: Record<string, unknown>) => void; /** Types a command into the session and runs it. */ runCommand: (command: string) => void; } -export interface TerminalDockProps { +export interface TerminalToolbarStatusProps { + host: Host; + isConnected: boolean; + /** Visible on a desktop viewport and connected: poll only while true. */ + active: boolean; +} + +export interface TerminalSidePanelProps { host: Host | undefined; hostId: number | undefined; hostLabel: string; - /** Whatever the opening action passed to openDock. */ - dockProps: Record<string, unknown>; + /** Whatever the opening action passed to openSidePanel. */ + panelProps: Record<string, unknown>; onClose: () => void; onRunInTerminal: (command: string) => void; } diff --git a/src/ui/features/terminal/terminal-types.ts b/plugins/ssh-terminal/src/frontend/terminal/terminal-types.ts similarity index 78% rename from src/ui/features/terminal/terminal-types.ts rename to plugins/ssh-terminal/src/frontend/terminal/terminal-types.ts index 6c9b24152..44d601195 100644 --- a/src/ui/features/terminal/terminal-types.ts +++ b/plugins/ssh-terminal/src/frontend/terminal/terminal-types.ts @@ -1,4 +1,4 @@ -import type { TerminalConfig } from "@/types"; +import type { TerminalConfig } from "../types"; export interface TerminalHostConfig { id?: number; @@ -33,6 +33,11 @@ export interface TerminalHandle { notifyResize: () => void; refresh: () => void; getApplicationCursorKeysMode: () => boolean; - openShareModal: () => void; - canShare: () => boolean; + /** What the shell's share action needs, or null while there is nothing to share. */ + getShareTarget: () => { + hostId: number; + sessionId: string; + protocol: "ssh"; + tabInstanceId?: string; + } | null; } diff --git a/src/ui/features/terminal/toolbar-geometry.ts b/plugins/ssh-terminal/src/frontend/terminal/toolbar-geometry.ts similarity index 91% rename from src/ui/features/terminal/toolbar-geometry.ts rename to plugins/ssh-terminal/src/frontend/terminal/toolbar-geometry.ts index 65a95a96e..548ed1901 100644 --- a/src/ui/features/terminal/toolbar-geometry.ts +++ b/plugins/ssh-terminal/src/frontend/terminal/toolbar-geometry.ts @@ -16,8 +16,6 @@ export type ToolbarDensity = "icon" | "labeled" | "expanded"; export const TOOLBAR_POSITION_STORAGE_KEY = "termix-terminal-toolbar-position-v2"; -export const AI_PANEL_POSITION_STORAGE_KEY = - "termix-terminal-ai-panel-position"; const TOOLBAR_MARGIN = 8; const RECOVERY_SIZE = 44; @@ -63,14 +61,6 @@ export function persistToolbarPosition(position: ToolbarPosition): void { persistPosition(TOOLBAR_POSITION_STORAGE_KEY, position); } -export function readStoredAiPanelPosition(): ToolbarPosition { - return readStoredPosition(AI_PANEL_POSITION_STORAGE_KEY); -} - -export function persistAiPanelPosition(position: ToolbarPosition): void { - persistPosition(AI_PANEL_POSITION_STORAGE_KEY, position); -} - export function clampToolbarPosition( position: ToolbarPosition, toolbarRect: RectLike, diff --git a/src/ui/features/terminal/touch-input-settings-store.ts b/plugins/ssh-terminal/src/frontend/terminal/touch-input-settings-store.ts similarity index 60% rename from src/ui/features/terminal/touch-input-settings-store.ts rename to plugins/ssh-terminal/src/frontend/terminal/touch-input-settings-store.ts index 4bd5e74f6..a571842b6 100644 --- a/src/ui/features/terminal/touch-input-settings-store.ts +++ b/plugins/ssh-terminal/src/frontend/terminal/touch-input-settings-store.ts @@ -1,16 +1,19 @@ -import { getTouchInputSettings } from "@/api/touch-input-settings-api"; +import type { PluginApiClient } from "@termix/plugin-sdk/frontend"; +import { getClientSettings } from "../terminal-api"; import { TOUCH_INPUT_DEFAULTS, type TouchInputSettings, -} from "@/types/touch-input-settings"; +} from "../../shared/touch-input-settings"; let cached: TouchInputSettings | undefined; let pending: Promise<TouchInputSettings> | undefined; -export function loadTouchInputSettings(): Promise<TouchInputSettings> { +export function loadTouchInputSettings( + api: PluginApiClient, +): Promise<TouchInputSettings> { if (cached) return Promise.resolve(cached); - pending ??= getTouchInputSettings() - .then((settings) => (cached = settings)) + pending ??= getClientSettings(api) + .then((settings) => (cached = settings.touchInput)) .catch(() => (cached = { ...TOUCH_INPUT_DEFAULTS })) .finally(() => { pending = undefined; diff --git a/src/ui/features/terminal/touch-wheel-coordinator.ts b/plugins/ssh-terminal/src/frontend/terminal/touch-wheel-coordinator.ts similarity index 99% rename from src/ui/features/terminal/touch-wheel-coordinator.ts rename to plugins/ssh-terminal/src/frontend/terminal/touch-wheel-coordinator.ts index 01dc6e358..64f30763d 100644 --- a/src/ui/features/terminal/touch-wheel-coordinator.ts +++ b/plugins/ssh-terminal/src/frontend/terminal/touch-wheel-coordinator.ts @@ -1,7 +1,7 @@ import { TOUCH_INPUT_DEFAULTS, type TouchInputSettings, -} from "@/types/touch-input-settings"; +} from "../../shared/touch-input-settings"; export interface TouchWheelPoint { clientX: number; diff --git a/plugins/ssh-terminal/src/frontend/types.ts b/plugins/ssh-terminal/src/frontend/types.ts new file mode 100644 index 000000000..2d21fddad --- /dev/null +++ b/plugins/ssh-terminal/src/frontend/types.ts @@ -0,0 +1,102 @@ +/** + * Host and terminal shapes as the terminal reads them. The plugin keeps its + * own copy instead of importing core's types; only the fields it uses matter, + * and the index signatures let the rest of a host record through. + */ + +export interface TerminalConfig { + localEcho?: "default" | "off" | "auto" | "on"; + cursorBlink: boolean; + cursorStyle: "block" | "underline" | "bar"; + fontSize: number; + fontFamily: string; + letterSpacing: number; + lineHeight: number; + theme: string; + + scrollback: number; + bellStyle: "none" | "sound" | "visual" | "both"; + rightClickSelectsWord: boolean; + macOptionIsMeta: boolean; + fastScrollModifier: "alt" | "ctrl" | "shift"; + fastScrollSensitivity: number; + minimumContrastRatio: number; + + backspaceMode: "normal" | "control-h"; + agentForwarding: boolean; + environmentVariables: Array<{ key: string; value: string }>; + startupSnippetId: number | null; + autoMosh: boolean; + moshCommand: string; + sudoPasswordAutoFill: boolean; + sudoPassword?: string | null; + keepaliveInterval?: number; + keepaliveCountMax?: number; + autoTmux: boolean; + syntaxHighlighting: boolean; + syntaxHighlightingOptions?: { + logLevels: boolean; + paths: boolean; + timestamps: boolean; + ipAddresses: boolean; + urls: boolean; + numbers: boolean; + }; + backgroundImage?: string; + backgroundImageOpacity?: number; + allowLegacyAlgorithms?: boolean; + linkClickBehavior?: "confirm" | "direct"; + useSSHTitle?: boolean; + agentSocketPath?: string; + agentIdentity?: string; + customThemeColors?: { + background: string; + foreground: string; + cursor?: string; + cursorAccent?: string; + selectionBackground?: string; + selectionForeground?: string; + black: string; + red: string; + green: string; + yellow: string; + blue: string; + magenta: string; + cyan: string; + white: string; + brightBlack: string; + brightRed: string; + brightGreen: string; + brightYellow: string; + brightBlue: string; + brightMagenta: string; + brightCyan: string; + brightWhite: string; + }; +} + +export type HostBackspaceMode = TerminalConfig["backspaceMode"]; + +/** A host as the shell hands it to a tab. */ +export interface Host { + id: number | string; + name?: string; + ip: string; + port: number; + username: string; + terminalConfig?: Partial<TerminalConfig> | null; + pluginSettings?: Record<string, Record<string, unknown>>; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string]: any; +} + +/** Enough of a snippet for the variables dialog. */ +export interface Snippet { + id: number; + name: string; + content: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string]: any; +} + +export type TabType = string; diff --git a/src/types/touch-input-settings.ts b/plugins/ssh-terminal/src/shared/touch-input-settings.ts similarity index 100% rename from src/types/touch-input-settings.ts rename to plugins/ssh-terminal/src/shared/touch-input-settings.ts diff --git a/plugins/ssh-terminal/tests/backend/activate.test.ts b/plugins/ssh-terminal/tests/backend/activate.test.ts new file mode 100644 index 000000000..7b7abe143 --- /dev/null +++ b/plugins/ssh-terminal/tests/backend/activate.test.ts @@ -0,0 +1,104 @@ +import { afterEach, describe, expect, it } from "vitest"; +import express from "express"; +import { + createMockCtx, + createTestDb, + type TestDb, +} from "@termix/plugin-sdk/testing"; +import { PluginCapabilityError } from "@termix/plugin-sdk/backend"; +import { activate } from "../../src/backend/index.js"; +import { hostImportNormalizer } from "../../src/backend/host-import.js"; +import type { + LiveSessionsV1, + TerminalHistoryV1, +} from "../../src/backend/services.js"; +import { manifest, pluginDir, startServer, type TestServer } from "./helpers"; + +let db: TestDb | null = null; +let server: TestServer | null = null; + +afterEach(async () => { + db?.close(); + db = null; + await server?.close(); + server = null; +}); + +describe("ssh-terminal activate", () => { + it("serves the terminal socket", async () => { + server = await startServer(); + expect(server.mock.wsRoutes).toEqual([{ path: "/terminal", raw: false }]); + }); + + it("provides sessions.live and terminal.history", async () => { + server = await startServer(); + const live = server.mock.services.get("sessions.live") as LiveSessionsV1; + expect(live.getSession("missing")).toBeNull(); + expect(live.listForUser("user-1")).toEqual([]); + expect(live.write("missing", "ls\r")).toBe(false); + expect(live.idleTimeoutMinutes()).toBe(30); + + await server.request("POST", "/command-history", { + body: { hostId: 1, command: "df -h" }, + }); + const history = server.mock.services.get( + "terminal.history", + ) as TerminalHistoryV1; + const listed = await server.mock.ctx.asUser("user-1", () => + history.list(1, 10), + ); + expect(listed.map((entry) => entry.command)).toEqual(["df -h"]); + }); + + it("keeps a plugin caller to its own actor's sessions", async () => { + server = await startServer(); + const live = server.mock.services.get("sessions.live") as LiveSessionsV1; + const other = await server.mock.ctx.asUser("user-2", async () => + live.listForUser("user-1"), + ); + expect(other).toEqual([]); + }); + + it("uses the admin session timeout for detached sessions", async () => { + server = await startServer({ settings: { sessionTimeoutMinutes: 90 } }); + const live = server.mock.services.get("sessions.live") as LiveSessionsV1; + expect(live.idleTimeoutMinutes()).toBe(90); + }); + + it("offers a host import normalizer", async () => { + server = await startServer(); + expect( + server.mock.ctx.registry.consume("ssh-terminal.hostImportNormalizer"), + ).toBe(hostImportNormalizer); + }); + + it.each(["db:own", "network:serve"])( + "fails closed without %s", + async (capability) => { + db = await createTestDb(pluginDir); + const mock = createMockCtx({ + pluginId: manifest.id, + manifest, + capabilities: manifest.capabilities.filter((c) => c !== capability), + db: db.database, + router: () => express.Router(), + }); + await expect(activate(mock.ctx)).rejects.toBeInstanceOf( + PluginCapabilityError, + ); + for (const dispose of [...mock.disposals].reverse()) await dispose(); + }, + ); +}); + +describe("host import", () => { + it("carries the terminal switches of an imported host", () => { + expect(hostImportNormalizer({ name: "web" })).toBeNull(); + expect( + hostImportNormalizer({ + enableTerminal: false, + enableCommandHistory: true, + }), + ).toEqual({ enableTerminal: false, enableCommandHistory: true }); + }); +}); diff --git a/plugins/ssh-terminal/tests/backend/helpers.ts b/plugins/ssh-terminal/tests/backend/helpers.ts new file mode 100644 index 000000000..851946a77 --- /dev/null +++ b/plugins/ssh-terminal/tests/backend/helpers.ts @@ -0,0 +1,92 @@ +import http from "node:http"; +import type { AddressInfo } from "node:net"; +import { fileURLToPath } from "node:url"; +import express, { type Router } from "express"; +import { + createMockCtx, + createTestDb, + type MockPluginContext, + type TestDb, +} from "@termix/plugin-sdk/testing"; +import type { PluginManifest } from "@termix/plugin-sdk/manifest"; +import manifestJson from "../../manifest.json"; + +export const pluginDir = fileURLToPath(new URL("../..", import.meta.url)); +export const manifest = manifestJson as unknown as PluginManifest; + +export interface TestServer { + db: TestDb; + mock: MockPluginContext; + request: ( + method: string, + path: string, + options?: { user?: string; body?: unknown }, + // Route bodies vary per test; asserting on them is the point. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ) => Promise<{ status: number; body: any }>; + close: () => Promise<void>; +} + +/** + * The plugin activated against a real in-memory database, its router mounted + * the way core mounts it, with the acting user taken from a test header. + */ +export async function startServer( + options: { permissions?: string[]; settings?: Record<string, unknown> } = {}, +): Promise<TestServer> { + const db = await createTestDb(pluginDir); + for (const user of ["user-1", "user-2"]) { + db.sqlite + .prepare("INSERT INTO users (id, username) VALUES (?, ?)") + .run(user, user); + } + db.sqlite.exec("INSERT INTO ssh_data (id) VALUES (1), (2)"); + + let router: Router | null = null; + const mock = createMockCtx({ + pluginId: manifest.id, + manifest, + capabilities: manifest.capabilities, + db: db.database, + router: () => (router = express.Router()), + permissions: options.permissions, + settings: options.settings, + }); + + const { activate } = await import("../../src/backend/index.js"); + await activate(mock.ctx); + + const app = express(); + app.use((req, _res, next) => { + mock.setActor(req.header("x-test-user") ?? undefined); + next(); + }); + app.use((req, res, next) => router!(req, res, next)); + + const server = http.createServer(app); + await new Promise<void>((resolve) => server.listen(0, resolve)); + const { port } = server.address() as AddressInfo; + + return { + db, + mock, + async request(method, path, { user = "user-1", body: given } = {}) { + const body = method === "GET" ? undefined : given; + const response = await fetch(`http://127.0.0.1:${port}${path}`, { + method, + headers: { + "x-test-user": user, + ...(body !== undefined ? { "content-type": "application/json" } : {}), + }, + body: body !== undefined ? JSON.stringify(body) : undefined, + }); + const text = await response.text(); + return { status: response.status, body: text ? JSON.parse(text) : null }; + }, + async close() { + await new Promise<void>((resolve) => server.close(() => resolve())); + for (const dispose of [...mock.disposals].reverse()) await dispose(); + db.close(); + }, + }; +} diff --git a/src/backend/tests/database/routes/terminal-image-storage-settings.test.ts b/plugins/ssh-terminal/tests/backend/images/image-storage-settings.test.ts similarity index 86% rename from src/backend/tests/database/routes/terminal-image-storage-settings.test.ts rename to plugins/ssh-terminal/tests/backend/images/image-storage-settings.test.ts index f2195c1e2..66bd1b969 100644 --- a/src/backend/tests/database/routes/terminal-image-storage-settings.test.ts +++ b/plugins/ssh-terminal/tests/backend/images/image-storage-settings.test.ts @@ -11,9 +11,9 @@ import { parseTerminalImageStorageMode, resolveTerminalImageStorageSettings, TERMINAL_IMAGE_STORAGE_KEYS, -} from "../../../database/routes/terminal-image-storage-settings.js"; -import { SettingsRepository } from "../../../database/repositories/settings-repository.js"; -import { TestSqliteDatabase } from "../repositories/test-support.js"; +} from "../../../src/backend/images/image-storage-settings.js"; +import { createFakeContext } from "@termix/plugin-sdk/testing"; +import { readImageStorageSettings } from "../../../src/backend/settings.js"; // parseImageLocalDir resolves against the host platform, so a POSIX literal // becomes a drive-rooted path on Windows. Compare against the same resolution. @@ -168,25 +168,24 @@ describe("terminal image storage settings", () => { expect(resolved.maxBytes).toBe(1_048_576); }); - it("reads persisted values through the real settings repository", async () => { - const adapter = new TestSqliteDatabase(); - try { - const context = await adapter.connect(); - const repository = new SettingsRepository(context); - await repository.set(TERMINAL_IMAGE_STORAGE_KEYS.mode, "local"); - await repository.set( - TERMINAL_IMAGE_STORAGE_KEYS.localDir, - "/persisted/images", - ); + it("reads the plugin's admin settings, and treats unset fields as unset", async () => { + const { ctx } = createFakeContext({ + settings: { + imageStorageMode: "local", + imageLocalDir: "/persisted/images", + }, + }); + const resolved = await readImageStorageSettings(ctx); + expect(resolved.mode).toBe("local"); + expect(resolved.localDir).toBe(localDir("/persisted/images")); + // Nothing stored and no environment: the built-in default. + expect(resolved.maxCount).toBe(DEFAULT_IMAGE_MAX_COUNT); + }); - const resolved = await resolveTerminalImageStorageSettings(repository, { - TERMIX_IMAGE_DIR: "/host-tmp/images", - }); - expect(resolved.mode).toBe("local"); - expect(resolved.localDir).toBe(localDir("/persisted/images")); - } finally { - await adapter.close(); - } + it("keeps TERMINAL_IMAGE_STORAGE_KEYS as the lookup names", () => { + expect(TERMINAL_IMAGE_STORAGE_KEYS.mode).toBe( + "terminal_image_storage_mode", + ); }); }); }); diff --git a/src/backend/tests/database/routes/terminal-image-storage.test.ts b/plugins/ssh-terminal/tests/backend/images/image-storage.test.ts similarity index 99% rename from src/backend/tests/database/routes/terminal-image-storage.test.ts rename to plugins/ssh-terminal/tests/backend/images/image-storage.test.ts index 2f15a5367..bdcee683d 100644 --- a/src/backend/tests/database/routes/terminal-image-storage.test.ts +++ b/plugins/ssh-terminal/tests/backend/images/image-storage.test.ts @@ -11,8 +11,8 @@ import { storeImageViaSftp, TerminalImageStorageError, type ImageSftpClient, -} from "../../../database/routes/terminal-image-storage.js"; -import type { TerminalImageStorageSettings } from "../../../database/routes/terminal-image-storage-settings.js"; +} from "../../../src/backend/images/image-storage.js"; +import type { TerminalImageStorageSettings } from "../../../src/backend/images/image-storage-settings.js"; const PNG_BYTES = Buffer.from([0x89, 0x50, 0x4e, 0x47]); diff --git a/src/backend/tests/database/routes/terminal-image-utils.test.ts b/plugins/ssh-terminal/tests/backend/images/image-utils.test.ts similarity index 97% rename from src/backend/tests/database/routes/terminal-image-utils.test.ts rename to plugins/ssh-terminal/tests/backend/images/image-utils.test.ts index 9837307fb..4157bf9ad 100644 --- a/src/backend/tests/database/routes/terminal-image-utils.test.ts +++ b/plugins/ssh-terminal/tests/backend/images/image-utils.test.ts @@ -6,7 +6,7 @@ import { createConcurrencyLimiter, isExpiredImage, isImageFilename, -} from "../../../database/routes/terminal-image-utils.js"; +} from "../../../src/backend/images/image-utils.js"; describe("terminal image utilities", () => { it("accepts UUID-based image filenames", () => { diff --git a/src/backend/tests/hosts/connect/terminal-prompt.test.ts b/plugins/ssh-terminal/tests/backend/keyboard-prompt.test.ts similarity index 74% rename from src/backend/tests/hosts/connect/terminal-prompt.test.ts rename to plugins/ssh-terminal/tests/backend/keyboard-prompt.test.ts index 512465512..d9d832bec 100644 --- a/src/backend/tests/hosts/connect/terminal-prompt.test.ts +++ b/plugins/ssh-terminal/tests/backend/keyboard-prompt.test.ts @@ -1,22 +1,70 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -vi.mock("../../../database/repositories/factory.js", () => ({ - createCurrentHostResolutionRepository: () => ({ - findCredentialByIdForUser: async () => null, - }), -})); +import type { + PluginKeyboardInteractiveDecision, + PluginKeyboardInteractivePrompt, + PluginSshHost, +} from "@termix/plugin-sdk/backend"; +import { SSHAuthManager } from "../../src/backend/keyboard-prompt.js"; -vi.mock("../../../utils/logger.js", () => ({ - sshLogger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() }, - authLogger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() }, -})); +/** + * Stands in for core's classifier (ctx.ssh.classifyKeyboardInteractive), + * which core's own tests cover. These tests are about what the terminal does + * with each decision: the socket messages, the answers and the timeouts. + */ +const PASSWORD = /password/i; +const FORTI = /type\s+['"]?push['"]?/i; +const PUSH = + /choose.*push.*totp|press enter.*(push|send)|push notification|authentication by phone/i; +const TOTP = + /verification code|verification_code|token|otp|2fa|authenticator|google.*auth/i; -import { SSHAuthManager } from "../../../hosts/connect/terminal-prompt.js"; +function classify( + round: { + name: string; + instructions: string; + prompts: PluginKeyboardInteractivePrompt[]; + }, + host: PluginSshHost, +): PluginKeyboardInteractiveDecision { + if (/warpgate/i.test(round.name)) { + return { + kind: "warpgate", + url: round.instructions.match(/https?:\/\/\S+/)?.[0] ?? null, + securityKey: round.instructions.match(/Security key: (\S+)/)?.[1] ?? "", + instructions: round.instructions, + }; + } + const texts = round.prompts.map((p) => p.prompt); + const isPush = + !texts.some((t) => FORTI.test(t)) && texts.some((t) => PUSH.test(t)); + if (!isPush) { + const totp = round.prompts.findIndex((p) => TOTP.test(p.prompt)); + if (totp !== -1) return { kind: "totp", promptIndex: totp }; + } + const stored = !!host.password && host.authType !== "none"; + const first = round.prompts.findIndex( + (p) => !(PASSWORD.test(p.prompt) && stored), + ); + if (first === -1) { + return { + kind: "auto", + responses: round.prompts.map((p) => + PASSWORD.test(p.prompt) ? String(host.password) : "", + ), + }; + } + return { kind: "input", promptIndex: first, isPush: PUSH.test(texts[first]) }; +} + +const log = { info: vi.fn(), success: vi.fn(), warn: vi.fn(), error: vi.fn() }; function createManager() { const sent: Record<string, unknown>[] = []; const ws = { send: (data: string) => sent.push(JSON.parse(data)) } as any; const manager = new SSHAuthManager({ + ssh: { classifyKeyboardInteractive: classify }, + log, userId: "user-1", ws, hostId: 1, diff --git a/plugins/ssh-terminal/tests/backend/memory-agent.test.ts b/plugins/ssh-terminal/tests/backend/memory-agent.test.ts new file mode 100644 index 000000000..cda511174 --- /dev/null +++ b/plugins/ssh-terminal/tests/backend/memory-agent.test.ts @@ -0,0 +1,105 @@ +import { describe, it, expect, vi } from "vitest"; +import { generateKeyPairSync } from "crypto"; +import ssh2Pkg, { type ParsedKey } from "ssh2"; +import { MemoryAgent } from "../../src/backend/helpers.js"; + +describe("MemoryAgent", () => { + it("serves identities and signatures over the agent protocol", async () => { + const { privateKey } = generateKeyPairSync("rsa", { modulusLength: 2048 }); + const parsed = ssh2Pkg.utils.parseKey( + privateKey.export({ type: "pkcs1", format: "pem" }), + ); + expect(parsed).not.toBeInstanceOf(Error); + + const agent = new MemoryAgent(parsed as ParsedKey); + const stream = await new Promise<NodeJS.ReadWriteStream>( + (resolve, reject) => { + agent.getStream((error, result) => { + if (error || !result) + reject(error ?? new Error("Missing agent stream")); + else resolve(result); + }); + }, + ); + const client = new ssh2Pkg.AgentProtocol(true); + client.pipe(stream).pipe(client); + + const identities = await new Promise<ParsedKey[]>((resolve, reject) => { + client.getIdentities((error, keys) => { + if (error || !keys) reject(error ?? new Error("Missing identities")); + else resolve(keys); + }); + }); + expect(identities).toHaveLength(1); + expect(identities[0].getPublicSSH()).toEqual( + (parsed as ParsedKey).getPublicSSH(), + ); + + const data = Buffer.from("forwarded-agent-test"); + const signature = await new Promise<Buffer>((resolve, reject) => { + client.sign(identities[0], data, (error, result) => { + if (error || !result) reject(error ?? new Error("Missing signature")); + else resolve(result); + }); + }); + expect((parsed as ParsedKey).verify(data, signature)).toBe(true); + + client.destroy(); + stream.destroy(); + }); +}); + +describe("MemoryAgent unsupported extension framing", () => { + it.each([false, true])( + "can enumerate keys after rejecting session-bind (fragmented=%s)", + async (fragmented) => { + const { privateKey } = generateKeyPairSync("rsa", { + modulusLength: 2048, + }); + const key = ssh2Pkg.utils.parseKey( + privateKey.export({ type: "pkcs1", format: "pem" }), + ) as ParsedKey; + const agent = new MemoryAgent(key); + const stream = await new Promise<NodeJS.ReadWriteStream>( + (resolve, reject) => { + agent.getStream((error, result) => + error || !result ? reject(error) : resolve(result), + ); + }, + ); + const replies: Buffer[] = []; + let buffered = Buffer.alloc(0); + stream.on("data", (data: Buffer) => { + buffered = Buffer.concat([buffered, data]); + while ( + buffered.length >= 4 && + buffered.length >= buffered.readUInt32BE(0) + 4 + ) { + const end = buffered.readUInt32BE(0) + 4; + replies.push(buffered.subarray(4, end)); + buffered = buffered.subarray(end); + } + }); + const extension = Buffer.from("session-bind@openssh.com"); + const request = Buffer.alloc(4 + 1 + 4 + extension.length); + request.writeUInt32BE(request.length - 4, 0); + request[4] = 27; + request.writeUInt32BE(extension.length, 5); + extension.copy(request, 9); + const identities = Buffer.from([0, 0, 0, 1, 11]); + try { + const requests = Buffer.concat([request, identities]); + if (fragmented) { + stream.write(requests.subarray(0, 7)); + stream.write(requests.subarray(7)); + } else stream.write(requests); + await vi.waitFor(() => expect(replies).toHaveLength(2)); + expect(replies[0][0]).toBe(5); // Unsupported extension remains refused. + expect(replies[1][0]).toBe(12); // The following identities request succeeds. + expect(replies[1].readUInt32BE(1)).toBe(1); + } finally { + stream.destroy(); + } + }, + ); +}); diff --git a/plugins/ssh-terminal/tests/backend/migrations.test.ts b/plugins/ssh-terminal/tests/backend/migrations.test.ts new file mode 100644 index 000000000..fde9d472e --- /dev/null +++ b/plugins/ssh-terminal/tests/backend/migrations.test.ts @@ -0,0 +1,110 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { createTestDb, type TestDb } from "@termix/plugin-sdk/testing"; +import { pluginDir } from "./helpers"; + +// command_history as core's SQLite bootstrap created it before 2.9.0, plus the +// index performance-indexes.ts added. +const LEGACY_DDL = ` + CREATE TABLE command_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT NOT NULL, + host_id INTEGER NOT NULL, + command TEXT NOT NULL, + executed_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, + FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE + ); + CREATE INDEX idx_command_history_user_host ON command_history (user_id, host_id); +`; + +let db: TestDb | null = null; + +afterEach(() => { + db?.close(); + db = null; +}); + +const tableExists = (name: string) => + !!db!.sqlite + .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?") + .get(name); + +const indexesOn = (table: string) => + ( + db!.sqlite + .prepare( + "SELECT name FROM sqlite_master WHERE type = 'index' AND tbl_name = ? AND name NOT LIKE 'sqlite_%'", + ) + .all(table) as { name: string }[] + ).map((row) => row.name); + +describe("adopting command_history", () => { + it("keeps every existing row and its index when the legacy table is there", async () => { + db = await createTestDb(pluginDir, { + before: (sqlite) => { + sqlite.exec("INSERT INTO users (id) VALUES ('user-1'), ('user-2')"); + sqlite.exec("INSERT INTO ssh_data (id) VALUES (1), (2)"); + sqlite.exec(LEGACY_DDL); + sqlite.exec(` + INSERT INTO command_history (user_id, host_id, command, executed_at) + VALUES ('user-1', 1, 'ls -la', '2026-01-01T00:00:00.000Z'), + ('user-2', 2, 'uptime', '2026-01-02T00:00:00.000Z'); + `); + }, + }); + + expect(db.applied).toEqual(["0001_adopt_command_history"]); + expect(tableExists("command_history")).toBe(false); + expect( + db.sqlite + .prepare( + "SELECT user_id, host_id, command, executed_at FROM p_ssh_terminal_command_history ORDER BY id", + ) + .all(), + ).toEqual([ + { + user_id: "user-1", + host_id: 1, + command: "ls -la", + executed_at: "2026-01-01T00:00:00.000Z", + }, + { + user_id: "user-2", + host_id: 2, + command: "uptime", + executed_at: "2026-01-02T00:00:00.000Z", + }, + ]); + expect(indexesOn("p_ssh_terminal_command_history")).toEqual([ + "idx_command_history_user_host", + ]); + }); + + it("creates a fresh table when there is no legacy one", async () => { + db = await createTestDb(pluginDir); + expect(tableExists("p_ssh_terminal_command_history")).toBe(true); + expect(indexesOn("p_ssh_terminal_command_history")).toEqual([ + "idx_command_history_user_host", + ]); + }); + + it("cascades when the user or the host is deleted", async () => { + db = await createTestDb(pluginDir, { + before: (sqlite) => { + sqlite.exec("INSERT INTO users (id) VALUES ('user-1'), ('user-2')"); + sqlite.exec("INSERT INTO ssh_data (id) VALUES (1), (2)"); + }, + }); + db.sqlite.exec(` + INSERT INTO p_ssh_terminal_command_history (user_id, host_id, command) + VALUES ('user-1', 1, 'a'), ('user-2', 1, 'b'), ('user-2', 2, 'c'); + `); + db.sqlite.exec("DELETE FROM users WHERE id = 'user-1'"); + db.sqlite.exec("DELETE FROM ssh_data WHERE id = 2"); + expect( + db.sqlite + .prepare("SELECT command FROM p_ssh_terminal_command_history") + .all(), + ).toEqual([{ command: "b" }]); + }); +}); diff --git a/plugins/ssh-terminal/tests/backend/routes.test.ts b/plugins/ssh-terminal/tests/backend/routes.test.ts new file mode 100644 index 000000000..a2d04f151 --- /dev/null +++ b/plugins/ssh-terminal/tests/backend/routes.test.ts @@ -0,0 +1,134 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { startServer, type TestServer } from "./helpers"; + +let server: TestServer | null = null; + +afterEach(async () => { + await server?.close(); + server = null; +}); + +const history = (s: TestServer) => + s.db.sqlite + .prepare( + "SELECT user_id, host_id, command FROM p_ssh_terminal_command_history ORDER BY id", + ) + .all(); + +describe("command history routes", () => { + it("saves, lists newest first without duplicates, deletes and clears", async () => { + server = await startServer(); + for (const command of ["ls", "pwd", "ls"]) { + const saved = await server.request("POST", "/command-history", { + body: { hostId: 1, command }, + }); + expect(saved.status).toBe(201); + } + await server.request("POST", "/command-history", { + user: "user-2", + body: { hostId: 1, command: "whoami" }, + }); + + const listed = await server.request("GET", "/command-history/1"); + expect(listed.body).toEqual(["ls", "pwd"]); + + await server.request("POST", "/command-history/delete", { + body: { hostId: 1, command: "pwd" }, + }); + expect((await server.request("GET", "/command-history/1")).body).toEqual([ + "ls", + ]); + + await server.request("DELETE", "/command-history/1"); + expect(history(server)).toEqual([ + { user_id: "user-2", host_id: 1, command: "whoami" }, + ]); + expect(server.db.persisted).toBeGreaterThan(0); + }); + + it("acknowledges but never stores a command that looks like a secret", async () => { + server = await startServer(); + const saved = await server.request("POST", "/command-history", { + body: { hostId: 1, command: "export API_TOKEN=abc" }, + }); + expect(saved.status).toBe(201); + expect(history(server)).toEqual([]); + }); + + it("stores nothing while command history is off globally", async () => { + server = await startServer({ settings: { commandHistoryEnabled: false } }); + await server.request("POST", "/command-history", { + body: { hostId: 1, command: "ls" }, + }); + expect(history(server)).toEqual([]); + }); + + it("stores nothing for a host that turned command history off", async () => { + server = await startServer(); + await server.mock.ctx.settings.setHost(2, "enableCommandHistory", false); + await server.request("POST", "/command-history", { + body: { hostId: 2, command: "ls" }, + }); + await server.request("POST", "/command-history", { + body: { hostId: 1, command: "ls" }, + }); + expect(history(server)).toEqual([ + { user_id: "user-1", host_id: 1, command: "ls" }, + ]); + }); + + it("rejects a request missing its fields", async () => { + server = await startServer(); + const saved = await server.request("POST", "/command-history", { + body: { hostId: 1 }, + }); + expect(saved.status).toBe(400); + }); + + it("needs hosts.view for the recent list", async () => { + server = await startServer({ permissions: [] }); + const denied = await server.request("GET", "/command-history/1/recent"); + expect(denied.status).toBe(403); + }); + + it("lists recent commands with duplicates for a hosts.view holder", async () => { + server = await startServer({ permissions: ["hosts.view"] }); + for (const command of ["uptime", "uptime"]) { + await server.request("POST", "/command-history", { + body: { hostId: 1, command }, + }); + } + const allowed = await server.request("GET", "/command-history/1/recent"); + expect(allowed.body).toEqual(["uptime", "uptime"]); + }); +}); + +describe("client settings and image storage", () => { + it("serves the terminal's settings to any signed-in user", async () => { + server = await startServer({ + settings: { sessionTimeoutMinutes: 45, sessionPersistence: false }, + }); + const { body } = await server.request("GET", "/client-settings"); + expect(body.sessionTimeoutMinutes).toBe(45); + expect(body.sessionPersistence).toBe(false); + expect(body.commandHistoryEnabled).toBe(true); + expect(body.touchInput.enabled).toBe(true); + }); + + it("refuses the image storage test to a non-admin", async () => { + server = await startServer({ permissions: [] }); + const denied = await server.request("POST", "/image-storage/test", { + body: { instanceId: "tab-1" }, + }); + expect(denied.status).toBe(403); + }); + + it("reports an unconnected session to an admin", async () => { + server = await startServer({ permissions: ["admin.plugins.manage"] }); + const result = await server.request("POST", "/image-storage/test", { + body: { instanceId: "tab-1" }, + }); + expect(result.status).toBe(200); + expect(result.body.connected).toBe(false); + }); +}); diff --git a/src/backend/tests/hosts/terminal/session-manager.test.ts b/plugins/ssh-terminal/tests/backend/session-manager.test.ts similarity index 84% rename from src/backend/tests/hosts/terminal/session-manager.test.ts rename to plugins/ssh-terminal/tests/backend/session-manager.test.ts index 41b96dd52..f15be1579 100644 --- a/src/backend/tests/hosts/terminal/session-manager.test.ts +++ b/plugins/ssh-terminal/tests/backend/session-manager.test.ts @@ -1,56 +1,27 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; +import { + TerminalSessionManager, + isMessageAllowedForParticipant, +} from "../../src/backend/session-manager.js"; +import type { RecordingSink } from "../../src/backend/services.js"; -// Stub all external imports before loading the module under test -const mockCreate = vi.fn().mockResolvedValue({ id: 1 }); -const mockUpdateEnded = vi.fn().mockResolvedValue(undefined); +// A recordings.writer provider that keeps what it was given. +const appends = vi.fn<(chunk: string) => Promise<void>>(); +const persists = vi.fn<RecordingSink["persist"]>(); +const discards = vi.fn(); +const opens = vi.fn(); -vi.mock("../../../database/db/index.js", () => ({ - getDb: () => ({}), -})); - -vi.mock("../../../database/repositories/factory.js", () => ({ - createCurrentSessionRecordingRepository: () => ({ - create: mockCreate, - updateEnded: mockUpdateEnded, - }), -})); - -vi.mock("../../../utils/logger.js", () => ({ - sshLogger: { - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - debug: vi.fn(), - }, -})); - -// Mock individual fs.promises methods via a stub object -const mockMkdir = vi.fn().mockResolvedValue(undefined); -const mockWriteFile = vi.fn().mockResolvedValue(undefined); -const mockAppendFile = vi.fn().mockResolvedValue(undefined); -const mockUnlink = vi.fn().mockResolvedValue(undefined); - -vi.mock("fs", () => ({ - default: { - promises: { - mkdir: mockMkdir, - writeFile: mockWriteFile, - appendFile: mockAppendFile, - readFile: vi.fn(), - unlink: mockUnlink, +const log = { info: vi.fn(), success: vi.fn(), warn: vi.fn(), error: vi.fn() }; +const sessionManager = new TerminalSessionManager({ + log, + getTimeoutMinutes: () => 30, + getRecordings: () => ({ + open: async (meta) => { + opens(meta); + return { append: appends, persist: persists, discard: discards }; }, - }, - promises: { - mkdir: mockMkdir, - writeFile: mockWriteFile, - appendFile: mockAppendFile, - readFile: vi.fn(), - unlink: mockUnlink, - }, -})); - -const { sessionManager, isMessageAllowedForParticipant } = - await import("../../../hosts/terminal/session-manager.js"); + }), +}); // Minimal fake WebSocket - only the surface session-manager touches. function makeFakeWs(readyState = 1 /* OPEN */) { @@ -67,11 +38,8 @@ const WS_CLOSED = 3; describe("TerminalSessionManager - session logging", () => { beforeEach(() => { vi.clearAllMocks(); - // Re-apply resolved values after clearAllMocks - mockMkdir.mockResolvedValue(undefined); - mockWriteFile.mockResolvedValue(undefined); - mockCreate.mockResolvedValue({ id: 1 }); - mockUpdateEnded.mockResolvedValue(undefined); + appends.mockResolvedValue(undefined); + persists.mockResolvedValue(undefined); }); it("createSession stores sessionLoggingEnabled=true by default", () => { @@ -96,7 +64,7 @@ describe("TerminalSessionManager - session logging", () => { sessionManager.destroySession(id); }); - it("does not write log file when sessionLoggingEnabled=false", async () => { + it("does not record when sessionLoggingEnabled=false", async () => { const id = sessionManager.createSession( "u1", 1, @@ -109,10 +77,11 @@ describe("TerminalSessionManager - session logging", () => { sessionManager.bufferOutput(id, "some output"); sessionManager.destroySession(id); await new Promise((r) => setTimeout(r, 20)); - expect(mockWriteFile).not.toHaveBeenCalled(); + expect(opens).not.toHaveBeenCalled(); + expect(appends).not.toHaveBeenCalled(); }); - it("writes log file and inserts DB row when sessionLoggingEnabled=true", async () => { + it("hands the recording to the recordings service in one batch, then persists it", async () => { const id = sessionManager.createSession( "u1", 1, @@ -123,10 +92,20 @@ describe("TerminalSessionManager - session logging", () => { true, ); sessionManager.bufferOutput(id, "terminal output data"); + sessionManager.bufferOutput(id, "more output"); + sessionManager.bufferInput(id, "ls\r"); sessionManager.destroySession(id); await new Promise((r) => setTimeout(r, 20)); - expect(mockWriteFile).toHaveBeenCalledOnce(); - expect(mockCreate).toHaveBeenCalledOnce(); + // One write for the whole burst, never one per chunk (issue #1049). + expect(appends).toHaveBeenCalledOnce(); + const chunk = appends.mock.calls[0][0]; + expect(chunk.split("\n")[0]).toContain('"version":2'); + expect(chunk).toContain("terminal output data"); + expect(chunk).toContain("more output"); + expect(persists).toHaveBeenCalledOnce(); + expect(opens).toHaveBeenCalledWith( + expect.objectContaining({ hostId: 1, userId: "u1", protocol: "ssh" }), + ); }); it("does not write log file when buffer is empty", async () => { @@ -141,7 +120,23 @@ describe("TerminalSessionManager - session logging", () => { ); sessionManager.destroySession(id); await new Promise((r) => setTimeout(r, 20)); - expect(mockWriteFile).not.toHaveBeenCalled(); + expect(appends).not.toHaveBeenCalled(); + expect(discards).toHaveBeenCalledOnce(); + }); + + it("records nothing when no recordings service is running", async () => { + const withoutRecordings = new TerminalSessionManager({ + log, + getTimeoutMinutes: () => 30, + getRecordings: () => null, + }); + const id = withoutRecordings.createSession("u1", 1, "host", 80, 24); + expect(withoutRecordings.getSession(id)?.sessionLoggingEnabled).toBe(false); + withoutRecordings.bufferOutput(id, "output"); + withoutRecordings.destroySession(id); + await new Promise((r) => setTimeout(r, 20)); + expect(appends).not.toHaveBeenCalled(); + withoutRecordings.destroyAll(); }); it("bufferOutput trims old data when exceeding 512KB", () => { @@ -166,10 +161,8 @@ describe("TerminalSessionManager - session logging", () => { describe("TerminalSessionManager - multiplayer participants", () => { beforeEach(() => { vi.clearAllMocks(); - mockMkdir.mockResolvedValue(undefined); - mockWriteFile.mockResolvedValue(undefined); - mockCreate.mockResolvedValue({ id: 1 }); - mockUpdateEnded.mockResolvedValue(undefined); + appends.mockResolvedValue(undefined); + persists.mockResolvedValue(undefined); }); function createConnectedSession(): string { diff --git a/plugins/ssh-terminal/tests/frontend/activate.test.tsx b/plugins/ssh-terminal/tests/frontend/activate.test.tsx index 74cae956e..247aac198 100644 --- a/plugins/ssh-terminal/tests/frontend/activate.test.tsx +++ b/plugins/ssh-terminal/tests/frontend/activate.test.tsx @@ -58,10 +58,36 @@ describe(`${manifest.id} activate`, () => { "terminal.listSessions", "terminal.sendToActive", "terminal.sendToSession", + "terminal.open", ]), ); }); + it("registers the command history panel and its rail item", async () => { + rendered = await renderWithApp(plugin, { manifest, locales }); + expect(rendered.registered.panels()).toContain("history"); + expect(rendered.registered.railItems().map((item) => item.id)).toContain( + "history", + ); + }); + + it("offers the local terminal only in the desktop app", async () => { + rendered = await renderWithApp(plugin, { manifest, locales }); + expect(rendered.registered.tabs()).not.toContain("local-terminal"); + await rendered.deactivate(); + + (window as { IS_ELECTRON?: boolean }).IS_ELECTRON = true; + try { + rendered = await renderWithApp(plugin, { manifest, locales }); + expect(rendered.registered.tabs()).toContain("local-terminal"); + expect(rendered.registered.railItems().map((item) => item.id)).toContain( + "local-terminal", + ); + } finally { + delete (window as { IS_ELECTRON?: boolean }).IS_ELECTRON; + } + }); + it("terminal.sendToActive and terminal.listSessions report no session when none is registered", async () => { rendered = await renderWithApp(plugin, { manifest, locales }); await expect( diff --git a/src/ui/tests/lib/keybinding-dispatch.test.ts b/plugins/ssh-terminal/tests/frontend/lib/keybinding-dispatch.test.ts similarity index 87% rename from src/ui/tests/lib/keybinding-dispatch.test.ts rename to plugins/ssh-terminal/tests/frontend/lib/keybinding-dispatch.test.ts index 0ad58d53a..80ef2b075 100644 --- a/src/ui/tests/lib/keybinding-dispatch.test.ts +++ b/plugins/ssh-terminal/tests/frontend/lib/keybinding-dispatch.test.ts @@ -1,8 +1,10 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; -import { dispatchKeybindingAction } from "../../lib/keybinding-dispatch"; +import { dispatchKeybindingAction } from "../../../src/frontend/lib/keybinding-dispatch"; import type { Terminal } from "@xterm/xterm"; -import type { KeybindingDispatchContext } from "../../lib/keybinding-dispatch"; -import * as actionRegistry from "../../shell/action-registry"; +import type { KeybindingDispatchContext } from "../../../src/frontend/lib/keybinding-dispatch"; +import * as sdkFrontend from "@termix/plugin-sdk/frontend"; + +vi.mock("@termix/plugin-sdk/frontend", () => ({ invokeAction: vi.fn() })); function makeContext( overrides: Partial<KeybindingDispatchContext> = {}, @@ -89,7 +91,7 @@ describe("dispatchKeybindingAction", () => { }); it("runSnippet resolves the snippet through the snippets.resolveForTerminal action and sends its content with a trailing return", async () => { - vi.spyOn(actionRegistry, "invokeAction").mockResolvedValue({ + vi.mocked(sdkFrontend.invokeAction).mockResolvedValue({ needsInputs: false, content: "uptime", }); @@ -97,7 +99,7 @@ describe("dispatchKeybindingAction", () => { dispatchKeybindingAction({ type: "runSnippet", snippetId: "1" }, ctx); await Promise.resolve(); await Promise.resolve(); - expect(actionRegistry.invokeAction).toHaveBeenCalledWith( + expect(sdkFrontend.invokeAction).toHaveBeenCalledWith( "snippets.resolveForTerminal", 1, null, @@ -106,7 +108,7 @@ describe("dispatchKeybindingAction", () => { }); it("runSnippet does nothing when the snippet no longer exists", async () => { - vi.spyOn(actionRegistry, "invokeAction").mockResolvedValue(undefined); + vi.mocked(sdkFrontend.invokeAction).mockResolvedValue(undefined); const ctx = makeContext(); dispatchKeybindingAction({ type: "runSnippet", snippetId: "1" }, ctx); await Promise.resolve(); @@ -115,7 +117,7 @@ describe("dispatchKeybindingAction", () => { }); it("runSnippet passes hostContext through to the resolve action", async () => { - vi.spyOn(actionRegistry, "invokeAction").mockResolvedValue({ + vi.mocked(sdkFrontend.invokeAction).mockResolvedValue({ needsInputs: false, content: "ping 10.0.0.5 -p 22", }); @@ -125,7 +127,7 @@ describe("dispatchKeybindingAction", () => { dispatchKeybindingAction({ type: "runSnippet", snippetId: "1" }, ctx); await Promise.resolve(); await Promise.resolve(); - expect(actionRegistry.invokeAction).toHaveBeenCalledWith( + expect(sdkFrontend.invokeAction).toHaveBeenCalledWith( "snippets.resolveForTerminal", 1, { ip: "10.0.0.5", username: "root", port: 22 }, @@ -134,7 +136,7 @@ describe("dispatchKeybindingAction", () => { }); it("runSnippet defers to onSnippetNeedsInputs instead of sending when the action reports needsInputs", async () => { - vi.spyOn(actionRegistry, "invokeAction").mockResolvedValue({ + vi.mocked(sdkFrontend.invokeAction).mockResolvedValue({ needsInputs: true, content: "echo $INPUT_1", }); diff --git a/src/ui/tests/lib/mac-line-navigation.test.ts b/plugins/ssh-terminal/tests/frontend/lib/mac-line-navigation.test.ts similarity index 93% rename from src/ui/tests/lib/mac-line-navigation.test.ts rename to plugins/ssh-terminal/tests/frontend/lib/mac-line-navigation.test.ts index ab4c8f6db..4bd7a3402 100644 --- a/src/ui/tests/lib/mac-line-navigation.test.ts +++ b/plugins/ssh-terminal/tests/frontend/lib/mac-line-navigation.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from "vitest"; -import { getMacLineNavigationSequence } from "../../lib/mac-line-navigation"; +import { getMacLineNavigationSequence } from "../../../src/frontend/lib/mac-line-navigation"; function keyEvent( init: Partial<KeyboardEvent> & Pick<KeyboardEvent, "code">, diff --git a/src/ui/lib/terminal-local-echo.test.ts b/plugins/ssh-terminal/tests/frontend/lib/terminal-local-echo.test.ts similarity index 95% rename from src/ui/lib/terminal-local-echo.test.ts rename to plugins/ssh-terminal/tests/frontend/lib/terminal-local-echo.test.ts index 180f91323..fb7e1e7b6 100644 --- a/src/ui/lib/terminal-local-echo.test.ts +++ b/plugins/ssh-terminal/tests/frontend/lib/terminal-local-echo.test.ts @@ -1,5 +1,8 @@ import { describe, expect, it } from "vitest"; -import { TerminalLocalEcho, resolveLocalEchoMode } from "./terminal-local-echo"; +import { + TerminalLocalEcho, + resolveLocalEchoMode, +} from "../../../src/frontend/lib/terminal-local-echo"; describe("TerminalLocalEcho", () => { it("renders immediately and suppresses the matching remote echo", () => { diff --git a/src/ui/tests/lib/terminal-syntax-highlighter.test.ts b/plugins/ssh-terminal/tests/frontend/lib/terminal-syntax-highlighter.test.ts similarity index 99% rename from src/ui/tests/lib/terminal-syntax-highlighter.test.ts rename to plugins/ssh-terminal/tests/frontend/lib/terminal-syntax-highlighter.test.ts index dfc2ec3a0..81f999f27 100644 --- a/src/ui/tests/lib/terminal-syntax-highlighter.test.ts +++ b/plugins/ssh-terminal/tests/frontend/lib/terminal-syntax-highlighter.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect } from "vitest"; import { highlightTerminalOutput, updateControlStringMode, -} from "../../lib/terminal-syntax-highlighter.js"; +} from "../../../src/frontend/lib/terminal-syntax-highlighter"; const ESC = "\x1b"; diff --git a/plugins/ssh-terminal/tests/frontend/settings/TouchInputSettings.test.tsx b/plugins/ssh-terminal/tests/frontend/settings/TouchInputSettings.test.tsx new file mode 100644 index 000000000..869e12985 --- /dev/null +++ b/plugins/ssh-terminal/tests/frontend/settings/TouchInputSettings.test.tsx @@ -0,0 +1,52 @@ +import { describe, expect, it, vi } from "vitest"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { TouchInputSettings } from "../../../src/frontend/settings/TouchInputSettings"; +import { TOUCH_INPUT_DEFAULTS } from "../../../src/shared/touch-input-settings"; + +function renderSettings(values: Record<string, unknown> = {}) { + const setValue = vi.fn(); + render( + <TouchInputSettings + pluginId="ssh-terminal" + values={values} + setValue={setValue} + running + />, + ); + return setValue; +} + +describe("TouchInputSettings", () => { + it("shows simple defaults, keeps tunables collapsed, and resets", async () => { + const user = userEvent.setup(); + const setValue = renderSettings({ + touchInput: { ...TOUCH_INPUT_DEFAULTS, dragThresholdPx: 20 }, + }); + + expect(screen.getByText("touchInput.enabled")).toBeTruthy(); + expect(screen.queryByLabelText("touchInput.dragThreshold")).toBeNull(); + await user.click(screen.getByText("touchInput.advanced")); + expect( + (screen.getByLabelText("touchInput.dragThreshold") as HTMLInputElement) + .value, + ).toBe("20"); + expect( + ( + screen.getByLabelText( + "touchInput.maximumTicksPerFrame", + ) as HTMLInputElement + ).value, + ).toBe("4"); + + await user.click(screen.getByText("touchInput.resetDefaults")); + expect(setValue).toHaveBeenLastCalledWith("touchInput", { + ...TOUCH_INPUT_DEFAULTS, + }); + }); + + it("falls back to the defaults when nothing is stored", () => { + renderSettings(); + expect(screen.getByText("touchInput.momentumEnabled")).toBeTruthy(); + }); +}); diff --git a/src/ui/tests/features/terminal/TerminalToolbar.test.tsx b/plugins/ssh-terminal/tests/frontend/terminal/TerminalToolbar.test.tsx similarity index 83% rename from src/ui/tests/features/terminal/TerminalToolbar.test.tsx rename to plugins/ssh-terminal/tests/frontend/terminal/TerminalToolbar.test.tsx index 6767241c8..3df6fdbe1 100644 --- a/src/ui/tests/features/terminal/TerminalToolbar.test.tsx +++ b/plugins/ssh-terminal/tests/frontend/terminal/TerminalToolbar.test.tsx @@ -10,76 +10,81 @@ import "@testing-library/jest-dom/vitest"; import userEvent from "@testing-library/user-event"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { StrictMode } from "react"; -import type { Host } from "@/types/ui-types"; +import type { Host } from "../../../src/frontend/types"; -const api = vi.hoisted(() => ({ - getServerMetricsById: vi.fn(), - startMetricsPolling: vi.fn(), - stopMetricsPolling: vi.fn(), -})); -const hostActionsApi = vi.hoisted(() => ({ - getSshActions: vi.fn(), -})); const mobileApi = vi.hoisted(() => ({ isMobile: false as boolean | undefined, })); - -vi.mock("@/api/host-metrics-status-api", () => api); -vi.mock("@/sidebar/tree/HostItem/HostItem", () => hostActionsApi); -vi.mock("@/hooks/use-mobile", () => ({ - useIsMobile: () => mobileApi.isMobile, -})); -const perms = vi.hoisted(() => ({ granted: [] as string[] })); -vi.mock("@/hooks/use-permissions", () => ({ - usePermissions: () => ({ - permissions: perms.granted, - isAdmin: false, - loaded: true, - has: (permission: string) => perms.granted.includes(permission), - }), +const slots = vi.hoisted(() => ({ + toolbar: [] as Array<{ actionId: string; titleKey: string }>, + invoked: [] as unknown[][], })); -vi.mock("react-i18next", () => ({ +const labels: Record<string, string> = { + "terminalToolbar.detachTmux": "Detach tmux", + "terminalToolbar.detachTmuxDescription": "Detach tmux session", + "terminalToolbar.image": "Image actions (images only)", + "terminalToolbar.upload": "Upload image", + "terminalToolbar.uploadImage": "Upload an image to the host", + "terminalToolbar.uploadingImage": "Uploading image…", + "terminalToolbar.paste": "Paste image", + "terminalToolbar.pasteImage": "Paste image from clipboard (images only)", + "terminalToolbar.showActions": "Show toolbar", + "terminalToolbar.showToolbar": "Expand toolbar", + "terminalToolbar.hideToolbar": "Hide toolbar", + "terminalToolbar.layout": "Toolbar display mode", + "terminalToolbar.layoutIcon": "Icon only", + "terminalToolbar.layoutLabeled": "Icons and labels", + "terminalToolbar.layoutExpanded": "Expanded with metrics", + "terminalToolbar.moveToolbar": "Move toolbar", + "terminalToolbar.moveToolbarHint": "Drag to reposition the toolbar", + "terminalToolbar.imageActionFailed": "Image action failed", + "terminalToolbar.retry": "Retry", + "terminalToolbar.dismissError": "Dismiss image error", + "ai.assistant": "AI Assistant", +}; + +vi.mock("@termix/plugin-sdk/frontend", async (importOriginal) => ({ + ...(await importOriginal<Record<string, unknown>>()), useTranslation: () => ({ - t: (key: string) => - ({ - "terminalToolbar.detachTmux": "Detach tmux", - "terminalToolbar.detachTmuxDescription": "Detach tmux session", - "terminalToolbar.image": "Image actions (images only)", - "terminalToolbar.upload": "Upload image", - "terminalToolbar.uploadImage": "Upload an image to the host", - "terminalToolbar.uploadingImage": "Uploading image…", - "terminalToolbar.paste": "Paste image", - "terminalToolbar.pasteImage": - "Paste image from clipboard (images only)", - "terminalToolbar.showActions": "Show toolbar", - "terminalToolbar.showToolbar": "Expand toolbar", - "terminalToolbar.hideToolbar": "Hide toolbar", - "terminalToolbar.layout": "Toolbar display mode", - "terminalToolbar.layoutIcon": "Icon only", - "terminalToolbar.layoutLabeled": "Icons and labels", - "terminalToolbar.layoutExpanded": "Expanded with metrics", - "terminalToolbar.moveToolbar": "Move toolbar", - "terminalToolbar.moveToolbarHint": "Drag to reposition the toolbar", - "terminalToolbar.imageActionFailed": "Image action failed", - "terminalToolbar.retry": "Retry", - "terminalToolbar.dismissError": "Dismiss image error", - "ai.assistant": "AI Assistant", - })[key] ?? key, + t: (key: string) => labels[key] ?? key, + language: "en", }), + useSlotContributions: (slotId: string) => + slotId === "terminal.toolbar" ? slots.toolbar : [], })); -import { TerminalToolbar } from "@/features/terminal/TerminalToolbar"; -import { - declareActionSlot, - registerAction, - registerSlotContribution, - resetActionRegistry, -} from "@/shell/action-registry"; +vi.mock("@termix/plugin-sdk/ui", async (importOriginal) => ({ + ...(await importOriginal<Record<string, unknown>>()), + useIsMobile: () => mobileApi.isMobile, + // The real slot reads the action registry; the toolbar only decides where + // it goes and what it is handed. + ActionSlot: ({ + context, + enabled, + }: { + context: () => unknown[]; + enabled?: boolean; + }) => + enabled === false + ? null + : slots.toolbar.map((contribution) => ( + <button + key={contribution.actionId} + type="button" + onClick={() => slots.invoked.push(context())} + > + {labels[contribution.titleKey] ?? contribution.titleKey} + </button> + )), + ComponentSlot: () => null, +})); + +import { TerminalToolbar } from "../../../src/frontend/terminal/TerminalToolbar"; import { clampToolbarPosition, getResponsiveToolbarDensity, -} from "@/features/terminal/toolbar-geometry"; +} from "../../../src/frontend/terminal/toolbar-geometry"; const host = { id: 7, connectionType: "ssh" } as unknown as Host; @@ -113,12 +118,8 @@ beforeEach(() => { value: 1024, }); localStorage.clear(); - perms.granted = []; - resetActionRegistry(); - hostActionsApi.getSshActions.mockReturnValue([]); - api.startMetricsPolling.mockResolvedValue({ viewerSessionId: "viewer" }); - api.getServerMetricsById.mockResolvedValue(null); - api.stopMetricsPolling.mockResolvedValue(undefined); + slots.toolbar = []; + slots.invoked = []; }); afterEach(() => { @@ -503,19 +504,11 @@ describe("TerminalToolbar Phase 1", () => { }); it("orders desktop actions and keeps the grab handle persistent", () => { - hostActionsApi.getSshActions.mockReturnValue([ - { type: "files", icon: () => null, label: "Files" }, - { type: "host-metrics", icon: () => null, label: "Host Metrics" }, - { type: "tmux_monitor", icon: () => null, label: "Tmux Monitor" }, - ]); const { container } = renderToolbar(); const labels = Array.from( container.querySelectorAll("[data-terminal-toolbar-wide] button"), ).map((button) => button.getAttribute("aria-label")); expect(labels).toEqual([ - "Files", - "Host Metrics", - "Tmux Monitor", "Detach tmux session", "Upload an image to the host", "Paste image from clipboard (images only)", @@ -798,7 +791,6 @@ describe("TerminalToolbar Phase 1", () => { expect( container.querySelector("[data-terminal-toolbar-host]"), ).not.toBeInTheDocument(); - expect(api.startMetricsPolling).not.toHaveBeenCalled(); expect(writes).not.toHaveBeenCalled(); expect(resizeObserver).not.toHaveBeenCalled(); expect(addWindowListener).not.toHaveBeenCalledWith( @@ -808,18 +800,6 @@ describe("TerminalToolbar Phase 1", () => { }, ); - it("mounts the toolbar and polls metrics once desktop is confirmed", async () => { - mobileApi.isMobile = false; - localStorage.setItem("termix-terminal-toolbar-density", "expanded"); - const { container } = renderToolbar(); - expect( - container.querySelector("[data-terminal-toolbar-host]"), - ).toBeInTheDocument(); - await waitFor(() => - expect(api.startMetricsPolling).toHaveBeenCalledWith(7), - ); - }); - it("ends dragging on pointer cancellation and lost capture, and rejects touch", () => { const { container } = renderToolbar(); const grab = screen.getByRole("button", { name: "Move toolbar" }); @@ -895,87 +875,45 @@ describe("TerminalToolbar Phase 1", () => { expect(screen.queryByRole("alert")).not.toBeInTheDocument(); }); - it("stops a metrics viewer that resolves after cleanup", async () => { - let resolveStart!: (value: { viewerSessionId: string }) => void; - api.startMetricsPolling.mockReturnValue( - new Promise((resolve) => { - resolveStart = resolve; - }), - ); - localStorage.setItem("termix-terminal-toolbar-density", "expanded"); - const view = renderToolbar(); - view.unmount(); - resolveStart({ viewerSessionId: "late-viewer" }); - await waitFor(() => - expect(api.stopMetricsPolling).toHaveBeenCalledWith(7, "late-viewer"), - ); - }); - describe("contributed toolbar actions", () => { - const ACTION = "ai.openWithContext"; - - function contribute() { - const handler = vi.fn(); - declareActionSlot({ id: "terminal.toolbar", accepts: ["button"] }); - registerAction(ACTION, handler, { permission: "ai.services.use" }); - registerSlotContribution("terminal.toolbar", { - actionId: ACTION, - titleKey: "ai.assistant", - }); - return handler; - } - it("shows no contributed button and no stray separator when nothing contributed", () => { - declareActionSlot({ id: "terminal.toolbar", accepts: ["button"] }); const { container } = renderToolbar(); - expect(screen.queryByRole("button", { name: "AI Assistant" })).toBeNull(); const separatorsWithout = container.querySelectorAll(".w-px").length; cleanup(); - contribute(); - perms.granted = ["ai.services.use"]; + slots.toolbar = [ + { actionId: "ai.openWithContext", titleKey: "ai.assistant" }, + ]; const withButton = renderToolbar().container; - expect(withButton.querySelectorAll(".w-px").length).toBe( separatorsWithout + 1, ); }); - it("hides the contributed button when the user lacks the permission", () => { - contribute(); - renderToolbar(); - - expect(screen.queryByRole("button", { name: "AI Assistant" })).toBeNull(); - }); - - it("invokes the action with the terminal slot api when permitted", async () => { - const handler = contribute(); - perms.granted = ["ai.services.use"]; - const getBufferText = vi.fn(() => "$ whoami\nroot"); + it("hands the action the terminal slot api", async () => { + slots.toolbar = [ + { actionId: "ai.openWithContext", titleKey: "ai.assistant" }, + ]; const slotApi = { host: undefined, - getBufferText, - openDock: vi.fn(), + getBufferText: vi.fn(() => "$ whoami\nroot"), + sessionId: () => null, + openSidePanel: vi.fn(), runCommand: vi.fn(), }; - renderToolbar({ slotApi }); - - expect(getBufferText).not.toHaveBeenCalled(); await userEvent.click( screen.getByRole("button", { name: "AI Assistant" }), ); - - expect(handler).toHaveBeenCalledWith(slotApi); + expect(slots.invoked).toEqual([[slotApi]]); }); it("hides the slot when the host has contributed actions disabled", () => { - contribute(); - perms.granted = ["ai.services.use"]; - + slots.toolbar = [ + { actionId: "ai.openWithContext", titleKey: "ai.assistant" }, + ]; renderToolbar({ actionsEnabled: false }); - expect(screen.queryByRole("button", { name: "AI Assistant" })).toBeNull(); }); }); diff --git a/src/ui/tests/features/terminal/android-hardware-keyboard.test.ts b/plugins/ssh-terminal/tests/frontend/terminal/android-hardware-keyboard.test.ts similarity index 92% rename from src/ui/tests/features/terminal/android-hardware-keyboard.test.ts rename to plugins/ssh-terminal/tests/frontend/terminal/android-hardware-keyboard.test.ts index 426c178af..ae4003300 100644 --- a/src/ui/tests/features/terminal/android-hardware-keyboard.test.ts +++ b/plugins/ssh-terminal/tests/frontend/terminal/android-hardware-keyboard.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { getAndroidHardwareKeySequence } from "@/features/terminal/android-hardware-keyboard"; +import { getAndroidHardwareKeySequence } from "../../../src/frontend/terminal/android-hardware-keyboard"; const key = ( value: string, diff --git a/src/ui/tests/features/terminal/android-ime.test.ts b/plugins/ssh-terminal/tests/frontend/terminal/android-ime.test.ts similarity index 100% rename from src/ui/tests/features/terminal/android-ime.test.ts rename to plugins/ssh-terminal/tests/frontend/terminal/android-ime.test.ts diff --git a/src/ui/tests/features/terminal/font-size-storage.test.ts b/plugins/ssh-terminal/tests/frontend/terminal/font-size-storage.test.ts similarity index 95% rename from src/ui/tests/features/terminal/font-size-storage.test.ts rename to plugins/ssh-terminal/tests/frontend/terminal/font-size-storage.test.ts index bd82b2ffd..7e42a02c4 100644 --- a/src/ui/tests/features/terminal/font-size-storage.test.ts +++ b/plugins/ssh-terminal/tests/frontend/terminal/font-size-storage.test.ts @@ -3,7 +3,7 @@ import { fontSizeStorageKey, readFontSize, saveFontSize, -} from "@/features/terminal/font-size-storage"; +} from "../../../src/frontend/terminal/font-size-storage"; beforeEach(() => localStorage.clear()); describe("persistent terminal zoom", () => { diff --git a/src/ui/tests/features/terminal/ios-rapid-typing.test.ts b/plugins/ssh-terminal/tests/frontend/terminal/ios-rapid-typing.test.ts similarity index 100% rename from src/ui/tests/features/terminal/ios-rapid-typing.test.ts rename to plugins/ssh-terminal/tests/frontend/terminal/ios-rapid-typing.test.ts diff --git a/src/ui/tests/features/terminal/terminal-buffer-text.test.ts b/plugins/ssh-terminal/tests/frontend/terminal/terminal-buffer-text.test.ts similarity index 96% rename from src/ui/tests/features/terminal/terminal-buffer-text.test.ts rename to plugins/ssh-terminal/tests/frontend/terminal/terminal-buffer-text.test.ts index ec5626a2e..e0b77ba6e 100644 --- a/src/ui/tests/features/terminal/terminal-buffer-text.test.ts +++ b/plugins/ssh-terminal/tests/frontend/terminal/terminal-buffer-text.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; import type { Terminal } from "@xterm/xterm"; -import { getTerminalBufferText } from "@/features/terminal/terminal-buffer-text"; +import { getTerminalBufferText } from "../../../src/frontend/terminal/terminal-buffer-text"; interface FakeLine { text: string; diff --git a/src/ui/tests/features/terminal/terminal-clipboard-actions.test.ts b/plugins/ssh-terminal/tests/frontend/terminal/terminal-clipboard-actions.test.ts similarity index 94% rename from src/ui/tests/features/terminal/terminal-clipboard-actions.test.ts rename to plugins/ssh-terminal/tests/frontend/terminal/terminal-clipboard-actions.test.ts index 00cc99c51..8837fed48 100644 --- a/src/ui/tests/features/terminal/terminal-clipboard-actions.test.ts +++ b/plugins/ssh-terminal/tests/frontend/terminal/terminal-clipboard-actions.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest"; import { resolveTerminalContextMenuAction, selectedTextToCopy, -} from "@/features/terminal/terminal-clipboard-actions"; +} from "../../../src/frontend/terminal/terminal-clipboard-actions"; describe("terminal clipboard actions", () => { it("copies a completed left-button selection when enabled", () => { diff --git a/src/ui/tests/features/terminal/terminal-clipboard.test.ts b/plugins/ssh-terminal/tests/frontend/terminal/terminal-clipboard.test.ts similarity index 98% rename from src/ui/tests/features/terminal/terminal-clipboard.test.ts rename to plugins/ssh-terminal/tests/frontend/terminal/terminal-clipboard.test.ts index a5e7899fb..a861c8a64 100644 --- a/src/ui/tests/features/terminal/terminal-clipboard.test.ts +++ b/plugins/ssh-terminal/tests/frontend/terminal/terminal-clipboard.test.ts @@ -3,7 +3,7 @@ import type { Terminal } from "@xterm/xterm"; const getCookieMock = vi.fn<(name: string) => string | undefined>(); -vi.mock("@/main-axios.ts", () => ({ +vi.mock("@termix/plugin-sdk/ui", () => ({ getCookie: (name: string) => getCookieMock(name), })); @@ -11,7 +11,7 @@ import { getUseRightClickCopyPaste, handleTerminalClipboardKeyEvent, createTerminalContextMenuHandler, -} from "@/features/terminal/terminal-clipboard"; +} from "../../../src/frontend/terminal/terminal-clipboard"; function createFakeTerminal({ selection = "" }: { selection?: string } = {}) { return { diff --git a/src/ui/tests/features/terminal/terminal-image-path.test.ts b/plugins/ssh-terminal/tests/frontend/terminal/terminal-image-path.test.ts similarity index 87% rename from src/ui/tests/features/terminal/terminal-image-path.test.ts rename to plugins/ssh-terminal/tests/frontend/terminal/terminal-image-path.test.ts index fb61a3e07..687c4c4ec 100644 --- a/src/ui/tests/features/terminal/terminal-image-path.test.ts +++ b/plugins/ssh-terminal/tests/frontend/terminal/terminal-image-path.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { quoteTerminalImagePath } from "@/features/terminal/terminal-image-path"; +import { quoteTerminalImagePath } from "../../../src/frontend/terminal/terminal-image-path"; describe("quoteTerminalImagePath", () => { it("preserves spaces as one shell argument", () => { diff --git a/src/ui/tests/features/terminal/terminal-image-upload-metadata.test.ts b/plugins/ssh-terminal/tests/frontend/terminal/terminal-image-upload-metadata.test.ts similarity index 93% rename from src/ui/tests/features/terminal/terminal-image-upload-metadata.test.ts rename to plugins/ssh-terminal/tests/frontend/terminal/terminal-image-upload-metadata.test.ts index 0ee277767..c0576f02f 100644 --- a/src/ui/tests/features/terminal/terminal-image-upload-metadata.test.ts +++ b/plugins/ssh-terminal/tests/frontend/terminal/terminal-image-upload-metadata.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { buildImageUploadFormData } from "@/features/terminal/terminal-image-upload"; +import { buildImageUploadFormData } from "../../../src/frontend/terminal/terminal-image-upload"; describe("buildImageUploadFormData", () => { it("tags a picked-file upload with source=file and a client timestamp", () => { diff --git a/src/ui/tests/features/terminal/terminal-key-event.test.ts b/plugins/ssh-terminal/tests/frontend/terminal/terminal-key-event.test.ts similarity index 95% rename from src/ui/tests/features/terminal/terminal-key-event.test.ts rename to plugins/ssh-terminal/tests/frontend/terminal/terminal-key-event.test.ts index 073abb484..2aebc9b57 100644 --- a/src/ui/tests/features/terminal/terminal-key-event.test.ts +++ b/plugins/ssh-terminal/tests/frontend/terminal/terminal-key-event.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest"; import { isPhysicalShortcutKey, isTabKeyEvent, -} from "@/features/terminal/terminal-key-event"; +} from "../../../src/frontend/terminal/terminal-key-event"; describe("isTabKeyEvent", () => { it.each([ diff --git a/src/ui/tests/features/terminal/touch-wheel-coordinator.test.ts b/plugins/ssh-terminal/tests/frontend/terminal/touch-wheel-coordinator.test.ts similarity index 98% rename from src/ui/tests/features/terminal/touch-wheel-coordinator.test.ts rename to plugins/ssh-terminal/tests/frontend/terminal/touch-wheel-coordinator.test.ts index 573ab493a..74e301b1b 100644 --- a/src/ui/tests/features/terminal/touch-wheel-coordinator.test.ts +++ b/plugins/ssh-terminal/tests/frontend/terminal/touch-wheel-coordinator.test.ts @@ -2,8 +2,8 @@ import { describe, expect, it, vi } from "vitest"; import { installTouchWheelCoordinator, TouchWheelCoordinator, -} from "../../../features/terminal/touch-wheel-coordinator"; -import { TOUCH_INPUT_DEFAULTS } from "@/types/touch-input-settings"; +} from "../../../src/frontend/terminal/touch-wheel-coordinator"; +import { TOUCH_INPUT_DEFAULTS } from "../../../src/shared/touch-input-settings"; const point = (clientY: number, clientX = 10) => ({ clientX, clientY }); diff --git a/src/ui/tests/features/terminal/touch-wheel-coordinator.xterm.test.ts b/plugins/ssh-terminal/tests/frontend/terminal/touch-wheel-coordinator.xterm.test.ts similarity index 97% rename from src/ui/tests/features/terminal/touch-wheel-coordinator.xterm.test.ts rename to plugins/ssh-terminal/tests/frontend/terminal/touch-wheel-coordinator.xterm.test.ts index a17655b89..485aa2902 100644 --- a/src/ui/tests/features/terminal/touch-wheel-coordinator.xterm.test.ts +++ b/plugins/ssh-terminal/tests/frontend/terminal/touch-wheel-coordinator.xterm.test.ts @@ -1,6 +1,6 @@ import { Terminal } from "@xterm/xterm"; import { afterEach, describe, expect, it, vi } from "vitest"; -import { installTouchWheelCoordinator } from "../../../features/terminal/touch-wheel-coordinator"; +import { installTouchWheelCoordinator } from "../../../src/frontend/terminal/touch-wheel-coordinator"; const touchEvent = (type: string, clientYValues: number[]) => { const event = new Event(type, { bubbles: true, cancelable: true }); diff --git a/plugins/ssh-terminal/tsconfig.json b/plugins/ssh-terminal/tsconfig.json index 1d3e42542..7b35314c3 100644 --- a/plugins/ssh-terminal/tsconfig.json +++ b/plugins/ssh-terminal/tsconfig.json @@ -1,10 +1,4 @@ { "extends": "@termix/plugin-sdk/tsconfig.plugin.json", - "compilerOptions": { - "paths": { - "@/types/*": ["../../src/types/*"], - "@/*": ["../../src/ui/*"] - } - }, "include": ["src/**/*.ts", "src/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"] } diff --git a/plugins/tailscale/src/frontend/TailscaleCheckOverlay.tsx b/plugins/tailscale/src/frontend/TailscaleCheckOverlay.tsx index 971880e2a..4e7e6cadf 100644 --- a/plugins/tailscale/src/frontend/TailscaleCheckOverlay.tsx +++ b/plugins/tailscale/src/frontend/TailscaleCheckOverlay.tsx @@ -1,6 +1,17 @@ import { useEffect, useRef, useState } from "react"; import { useTranslation } from "@termix/plugin-sdk/frontend"; -import type { TerminalOverlayProps } from "@/features/terminal/terminal-slots"; + +/** What the ssh-terminal plugin hands a "terminal.overlay" component. */ +interface TerminalOverlayProps { + host: { id: number | string; [key: string]: unknown } | undefined; + backgroundColor?: string; + subscribe: ( + listener: (message: { type: string; [key: string]: unknown }) => void, + ) => () => void; + holdConnectTimeout: (held: boolean) => void; + fail: (message: string) => void; + disconnect: () => void; +} import { TailscaleCheckDialog } from "./TailscaleCheckDialog"; /** How long Tailscale may hold a connection open waiting on approval. */ diff --git a/scripts/check-shell-plugin-ids.cjs b/scripts/check-shell-plugin-ids.cjs index d9fcdf75e..594d286bf 100644 --- a/scripts/check-shell-plugin-ids.cjs +++ b/scripts/check-shell-plugin-ids.cjs @@ -8,8 +8,7 @@ * needs from a plugin comes through the registries instead. * * A few literals are data rather than knowledge of a plugin: host protocol - * names that are columns on the host until Phase B moves them, and the - * terminal's own websocket, which lives in core until the terminal moves. + * names that are columns on the host until Phase B moves them. * Those are listed with a reason in scripts/shell-plugin-id-allowlist.json, * which fails too when an entry stops being needed, so it only shrinks. */ diff --git a/scripts/plugin-boundary-allowlist.json b/scripts/plugin-boundary-allowlist.json index ec6de3e0e..619466288 100644 --- a/scripts/plugin-boundary-allowlist.json +++ b/scripts/plugin-boundary-allowlist.json @@ -5,7 +5,6 @@ "plugins/ai/src/frontend/AiProviderSettings.tsx", "plugins/ai/src/frontend/ProposalCard.tsx", "plugins/ai/src/frontend/ai-api.ts", - "plugins/ai/src/frontend/index.tsx", "plugins/ai/src/frontend/settings/AiUserSettings.tsx", "plugins/ai/src/frontend/terminal/TerminalAiPanel.tsx", "plugins/ai/src/frontend/terminal/TerminalProposalCard.tsx", @@ -122,11 +121,7 @@ "plugins/remote-desktop/src/frontend/index.tsx", "plugins/remote-desktop/src/frontend/quick-connect-guac-host.ts", "plugins/remote-desktop/src/frontend/rdp-credential-prompt.ts", - "plugins/ssh-terminal/src/frontend/TerminalTabWithRegistry.tsx", - "plugins/ssh-terminal/src/frontend/index.tsx", - "plugins/ssh-terminal/src/frontend/session-registry.ts", "plugins/tailscale/src/frontend/TailscaleCheckDialog.tsx", - "plugins/tailscale/src/frontend/TailscaleCheckOverlay.tsx", "plugins/tailscale/src/frontend/TailscaleDevicesPanel.tsx", "plugins/tailscale/src/frontend/TailscaleDevicesStatus.tsx", "plugins/tailscale/src/frontend/TailscaleManagerShell.tsx", @@ -202,7 +197,6 @@ "plugins/remote-desktop/src/backend/guacamole-server.ts", "plugins/remote-desktop/src/backend/routes.ts", "plugins/remote-desktop/src/backend/token-service.ts", - "plugins/ssh-terminal/src/backend/index.ts", "plugins/tailscale/src/backend/host-metrics-manager.ts", "plugins/tailscale/src/backend/routes.ts" ], diff --git a/scripts/shell-plugin-id-allowlist.json b/scripts/shell-plugin-id-allowlist.json index 9d7c1ae91..ca5c29612 100644 --- a/scripts/shell-plugin-id-allowlist.json +++ b/scripts/shell-plugin-id-allowlist.json @@ -1,10 +1,6 @@ { - "$comment": "Literals src/ui may still spell that name a plugin id or a plugin-owned view. Each is data or the core terminal, not knowledge of a plugin. Checked by scripts/check-shell-plugin-ids.cjs; D1 and Phase B empty it.", + "$comment": "Literals src/ui may still spell that name a plugin id or a plugin-owned view. Each is data, not knowledge of a plugin. Checked by scripts/check-shell-plugin-ids.cjs; D1 and Phase B empty it.", "files": { - "src/ui/AppShell.tsx": { - "literals": ["terminal"], - "reason": "The terminal tab type. The terminal component, split view and file-manager hooks stay in core until the terminal's Phase B step; the ssh-terminal plugin only decides whether it is offered." - }, "src/ui/api/collab-api.ts": { "literals": ["rdp", "telnet", "vnc"], "reason": "Host protocol value (connectionType, enableRdp, session protocol), a host column until Phase B moves it. Host protocol value (connectionType, enableTelnet, session protocol), a host column until Phase B moves it. Host protocol value (connectionType, enableVnc, session protocol), a host column until Phase B moves it." @@ -17,70 +13,14 @@ "literals": ["rdp", "telnet", "vnc"], "reason": "Host protocol value (connectionType, enableRdp, session protocol), a host column until Phase B moves it. Host protocol value (connectionType, enableTelnet, session protocol), a host column until Phase B moves it. Host protocol value (connectionType, enableVnc, session protocol), a host column until Phase B moves it." }, - "src/ui/contexts/ConnectionDefaultsContext.tsx": { - "literals": ["rdp", "terminal"], - "reason": "Host protocol value (connectionType, enableRdp, session protocol), a host column until Phase B moves it. The terminal tab type. The terminal component, split view and file-manager hooks stay in core until the terminal's Phase B step; the ssh-terminal plugin only decides whether it is offered." - }, "src/ui/features/collab/CollabRoomTab.tsx": { - "literals": ["rdp", "ssh-terminal", "telnet", "vnc"], - "reason": "Host protocol value (connectionType, enableRdp, session protocol), a host column until Phase B moves it. Host protocol value (connectionType, enableTelnet, session protocol), a host column until Phase B moves it. Host protocol value (connectionType, enableVnc, session protocol), a host column until Phase B moves it. The core terminal opens its own websocket, served by the ssh-terminal plugin, until the terminal moves into it in Phase B." - }, - "src/ui/features/homepage/widgets/QuickConnectWidget.tsx": { - "literals": ["files", "terminal"], - "reason": "The terminal tab type. The terminal component, split view and file-manager hooks stay in core until the terminal's Phase B step; the ssh-terminal plugin only decides whether it is offered. A default widget config value (connectionTypes), not a code branch on the plugin: quick connect offers terminal and files by default regardless of which plugins are enabled." - }, - "src/ui/features/session-sharing/SharedSessionView.tsx": { - "literals": ["ssh-terminal"], - "reason": "The core terminal opens its own websocket, served by the ssh-terminal plugin, until the terminal moves into it in Phase B." - }, - "src/ui/features/terminal/Terminal.tsx": { - "literals": ["ssh-terminal", "terminal"], - "reason": "The core terminal opens its own websocket, served by the ssh-terminal plugin, until the terminal moves into it in Phase B. The terminal tab type. The terminal component, split view and file-manager hooks stay in core until the terminal's Phase B step; the ssh-terminal plugin only decides whether it is offered." - }, - "src/ui/features/terminal/TerminalTabContent.tsx": { - "literals": ["terminal"], - "reason": "The terminal tab type. The terminal component, split view and file-manager hooks stay in core until the terminal's Phase B step; the ssh-terminal plugin only decides whether it is offered." - }, - "src/ui/features/terminal/TerminalToolbar.tsx": { - "literals": ["files", "terminal"], - "reason": "The terminal tab type. The terminal component, split view and file-manager hooks stay in core until the terminal's Phase B step; the ssh-terminal plugin only decides whether it is offered. The toolbar's \"open file manager here\" button needs the terminal's current working directory before opening the tab, so it special-cases the files host action instead of using the generic opener; folds into the terminal's own Phase B step." - }, - "src/ui/features/tmux-monitor/TmuxMonitor.tsx": { - "literals": ["terminal"], - "reason": "The terminal tab type. The terminal component, split view and file-manager hooks stay in core until the terminal's Phase B step; the ssh-terminal plugin only decides whether it is offered." + "literals": ["rdp", "telnet", "vnc"], + "reason": "Host protocol value (connectionType, enableRdp, session protocol), a host column until Phase B moves it. Host protocol value (connectionType, enableTelnet, session protocol), a host column until Phase B moves it. Host protocol value (connectionType, enableVnc, session protocol), a host column until Phase B moves it." }, "src/ui/lib/connection-origin.ts": { "literals": ["rdp", "telnet", "vnc"], "reason": "Host protocol value (connectionType, enableRdp, session protocol), a host column until Phase B moves it. Host protocol value (connectionType, enableTelnet, session protocol), a host column until Phase B moves it. Host protocol value (connectionType, enableVnc, session protocol), a host column until Phase B moves it." }, - "src/ui/lib/host-connection-tabs.ts": { - "literals": ["terminal"], - "reason": "The terminal tab type. The terminal component, split view and file-manager hooks stay in core until the terminal's Phase B step; the ssh-terminal plugin only decides whether it is offered." - }, - "src/ui/onboarding/steps/WelcomeStep.tsx": { - "literals": ["files", "terminal"], - "reason": "The terminal tab type. The terminal component, split view and file-manager hooks stay in core until the terminal's Phase B step; the ssh-terminal plugin only decides whether it is offered. The fixed 3-item first-run welcome card (hosts, terminal, files) is core promotional copy, not an extensible list a plugin contributes to." - }, - "src/ui/shell/TabBar.tsx": { - "literals": ["terminal"], - "reason": "The terminal tab type. The terminal component, split view and file-manager hooks stay in core until the terminal's Phase B step; the ssh-terminal plugin only decides whether it is offered." - }, - "src/ui/shell/TabContext.tsx": { - "literals": ["terminal"], - "reason": "The terminal tab type. The terminal component, split view and file-manager hooks stay in core until the terminal's Phase B step; the ssh-terminal plugin only decides whether it is offered." - }, - "src/ui/sidebar/ConnectionDefaultsSettings.tsx": { - "literals": ["rdp", "terminal"], - "reason": "Host protocol value (connectionType, enableRdp, session protocol), a host column until Phase B moves it. The terminal tab type. The terminal component, split view and file-manager hooks stay in core until the terminal's Phase B step; the ssh-terminal plugin only decides whether it is offered." - }, - "src/ui/sidebar/ConnectionsPanel.tsx": { - "literals": ["terminal"], - "reason": "The terminal tab type. The terminal component, split view and file-manager hooks stay in core until the terminal's Phase B step; the ssh-terminal plugin only decides whether it is offered." - }, - "src/ui/sidebar/HostEditor.tsx": { - "literals": ["terminal"], - "reason": "The terminal tab type. The terminal component, split view and file-manager hooks stay in core until the terminal's Phase B step; the ssh-terminal plugin only decides whether it is offered." - }, "src/ui/sidebar/HostEditorData.ts": { "literals": ["docker", "rdp", "telnet", "vnc"], "reason": "Host data: the enableDocker column filter and the container runtime name, until Phase B moves them. Host protocol value (connectionType, enableRdp, session protocol), a host column until Phase B moves it. Host protocol value (connectionType, enableTelnet, session protocol), a host column until Phase B moves it. Host protocol value (connectionType, enableVnc, session protocol), a host column until Phase B moves it." @@ -89,25 +29,17 @@ "literals": ["rdp", "telnet", "vnc"], "reason": "Host protocol value (connectionType, enableRdp, session protocol), a host column until Phase B moves it. Host protocol value (connectionType, enableTelnet, session protocol), a host column until Phase B moves it. Host protocol value (connectionType, enableVnc, session protocol), a host column until Phase B moves it." }, - "src/ui/sidebar/HostManagerTabs.tsx": { - "literals": ["terminal"], - "reason": "The terminal tab type. The terminal component, split view and file-manager hooks stay in core until the terminal's Phase B step; the ssh-terminal plugin only decides whether it is offered." - }, "src/ui/sidebar/HostsPanel.tsx": { - "literals": ["docker", "rdp", "telnet", "terminal", "vnc"], - "reason": "Host data: the enableDocker column filter and the container runtime name, until Phase B moves them. Host protocol value (connectionType, enableRdp, session protocol), a host column until Phase B moves it. Host protocol value (connectionType, enableTelnet, session protocol), a host column until Phase B moves it. Host protocol value (connectionType, enableVnc, session protocol), a host column until Phase B moves it. The terminal tab type. The terminal component, split view and file-manager hooks stay in core until the terminal's Phase B step; the ssh-terminal plugin only decides whether it is offered." + "literals": ["docker", "rdp", "telnet", "vnc"], + "reason": "Host data: the enableDocker column filter and the container runtime name, until Phase B moves them. Host protocol value (connectionType, enableRdp, session protocol), a host column until Phase B moves it. Host protocol value (connectionType, enableTelnet, session protocol), a host column until Phase B moves it. Host protocol value (connectionType, enableVnc, session protocol), a host column until Phase B moves it." }, "src/ui/sidebar/QuickConnectPanel.tsx": { - "literals": ["files", "rdp", "terminal", "vnc"], - "reason": "Host protocol value (connectionType, enableRdp, session protocol), a host column until Phase B moves it. Host protocol value (connectionType, enableVnc, session protocol), a host column until Phase B moves it. The terminal tab type. The terminal component, split view and file-manager hooks stay in core until the terminal's Phase B step; the ssh-terminal plugin only decides whether it is offered. The quick-connect-for-an-unsaved-host flow offers a fixed terminal/files button pair, not an extensible list a plugin contributes to." + "literals": ["files", "rdp", "vnc"], + "reason": "Host protocol value (connectionType, enableRdp, session protocol), a host column until Phase B moves it. Host protocol value (connectionType, enableVnc, session protocol), a host column until Phase B moves it. The quick-connect-for-an-unsaved-host flow offers a fixed Files button next to the default connect button, not an extensible list a plugin contributes to." }, "src/ui/sidebar/quick-connect-host.ts": { "literals": ["rdp", "vnc"], "reason": "Host protocol value (connectionType, enableRdp, session protocol), a host column until Phase B moves it. Host protocol value (connectionType, enableVnc, session protocol), a host column until Phase B moves it." - }, - "src/ui/sidebar/tree/HostItem/HostItem.tsx": { - "literals": ["terminal"], - "reason": "The terminal tab type. The terminal component, split view and file-manager hooks stay in core until the terminal's Phase B step; the ssh-terminal plugin only decides whether it is offered." } } } diff --git a/src/backend/auth/legacy-providers.ts b/src/backend/auth/legacy-providers.ts index 60f8fe1aa..6d04dc1c5 100644 --- a/src/backend/auth/legacy-providers.ts +++ b/src/backend/auth/legacy-providers.ts @@ -108,6 +108,16 @@ function issuedCertificateProvider( request.requestOrigin, ); }, + cancelInteraction: async (request) => { + if (!request.requestId) return; + if (type === "stepca") { + const { cancelStepCaAuth } = await import("../hosts/step-ca-auth.js"); + cancelStepCaAuth(request.requestId); + return; + } + const { cancelAuthSession } = await import("../hosts/opkssh-auth.js"); + cancelAuthSession(request.requestId); + }, }; } @@ -202,6 +212,12 @@ const vaultProvider: SshAuthProvider = { request.requestOrigin, ); }, + cancelInteraction: async (request) => { + if (request.hostId === undefined) return; + const { cancelVaultAuthByHost } = + await import("../hosts/vault-oidc-auth.js"); + cancelVaultAuthByHost(request.userId, request.hostId); + }, }; const WARPGATE_PATTERN = /warpgate\s+authentication/i; diff --git a/src/backend/database/database.ts b/src/backend/database/database.ts index af607274d..749f84ce3 100644 --- a/src/backend/database/database.ts +++ b/src/backend/database/database.ts @@ -10,7 +10,6 @@ import hostRoutes from "./routes/host.js"; import alertRoutes from "./routes/alerts.js"; import credentialsRoutes from "./routes/credentials.js"; import sshAuthRoutes from "./routes/ssh-auth-routes.js"; -import terminalRoutes from "./routes/terminal.js"; import sessionLogRoutes from "./routes/session-log-routes.js"; import sessionSharingRoutes from "../hosts/session-sharing/routes.js"; import collabRoutes from "../hosts/collab/routes.js"; @@ -1808,7 +1807,6 @@ app.use("/host", hostRoutes); app.use("/alerts", alertRoutes); app.use("/credentials", credentialsRoutes); app.use("/ssh-auth", sshAuthRoutes); -app.use("/terminal", terminalRoutes); app.use("/session_logs", sessionLogRoutes); app.use("/session-sharing", sessionSharingRoutes); app.use("/collab", collabRoutes); diff --git a/src/backend/database/db/index.ts b/src/backend/database/db/index.ts index dc61483af..6b33ba122 100644 --- a/src/backend/database/db/index.ts +++ b/src/backend/database/db/index.ts @@ -419,15 +419,8 @@ async function initializeCompleteDatabase(): Promise<void> { FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE ); - CREATE TABLE IF NOT EXISTS command_history ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - host_id INTEGER NOT NULL, - command TEXT NOT NULL, - executed_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 - ); + -- command_history is owned by the ssh-terminal plugin, which adopts it + -- as p_ssh_terminal_command_history in its first migration. CREATE TABLE IF NOT EXISTS host_access ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -858,35 +851,8 @@ async function initializeCompleteDatabase(): Promise<void> { `); - try { - const timeoutRow = sqlite - .prepare( - "SELECT value FROM settings WHERE key = 'terminal_session_timeout_minutes'", - ) - .get() as { value: string } | undefined; - const timeoutMinutes = timeoutRow - ? parseInt(timeoutRow.value, 10) - : 30; - const ttlMs = - !isNaN(timeoutMinutes) && timeoutMinutes > 0 - ? timeoutMinutes * 60_000 - : 30 * 60_000; - const cutoff = new Date(Date.now() - ttlMs).toISOString(); - const result = sqlite - .prepare("DELETE FROM user_open_tabs WHERE updated_at <= ?") - .run(cutoff); - if (result.changes > 0) { - databaseLogger.info("Expired open tabs cleared on startup", { - operation: "db_init_open_tabs_cleanup", - count: result.changes, - }); - } - } catch (e) { - databaseLogger.warn("Could not clear expired open tabs on startup", { - operation: "db_init_open_tabs_cleanup_failed", - error: e, - }); - } + // Expired open tabs are dropped when the tab list is read, against the + // terminal plugin's own session timeout. try { const result = sqlite diff --git a/src/backend/database/db/performance-indexes.ts b/src/backend/database/db/performance-indexes.ts index 7e8269b32..46f121bd3 100644 --- a/src/backend/database/db/performance-indexes.ts +++ b/src/backend/database/db/performance-indexes.ts @@ -105,14 +105,9 @@ export const PERFORMANCE_INDEXES: PerformanceIndex[] = [ table: "recent_activity", columns: "user_id, timestamp", }, - { - name: "idx_command_history_user_host", - table: "command_history", - columns: "user_id, host_id", - }, - // file_manager_recent, file_manager_pinned, file_manager_shortcuts and - // transfer_recent are owned by the file-manager plugin now; its adopted - // table definitions carry these same indexes across the rename. + // file_manager_recent, file_manager_pinned, file_manager_shortcuts, + // transfer_recent and command_history are owned by plugins now; their + // adopted table definitions carry these same indexes across the rename. { name: "idx_user_open_tabs_user_id", table: "user_open_tabs", diff --git a/src/backend/database/db/schema.mysql.ts b/src/backend/database/db/schema.mysql.ts index da726bba4..7db8b3ccf 100644 --- a/src/backend/database/db/schema.mysql.ts +++ b/src/backend/database/db/schema.mysql.ts @@ -515,26 +515,6 @@ export const recentActivity = mysqlTable( ], ); -export const commandHistory = mysqlTable( - "command_history", - { - 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" }), - command: text("command").notNull(), - executedAt: text("executed_at") - .notNull() - .default(sql`(CURRENT_TIMESTAMP)`), - }, - (table) => [ - index("idx_command_history_user_host").on(table.userId, table.hostId), - ], -); - export const hostAccess = mysqlTable( "host_access", { diff --git a/src/backend/database/db/schema.pg.ts b/src/backend/database/db/schema.pg.ts index 8adef3f8f..9d0d515ef 100644 --- a/src/backend/database/db/schema.pg.ts +++ b/src/backend/database/db/schema.pg.ts @@ -516,26 +516,6 @@ export const recentActivity = pgTable( ], ); -export const commandHistory = pgTable( - "command_history", - { - 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" }), - command: text("command").notNull(), - executedAt: text("executed_at") - .notNull() - .default(sql`CURRENT_TIMESTAMP`), - }, - (table) => [ - index("idx_command_history_user_host").on(table.userId, table.hostId), - ], -); - export const hostAccess = pgTable( "host_access", { diff --git a/src/backend/database/db/schema.ts b/src/backend/database/db/schema.ts index 1fe5c19f8..f6139a256 100644 --- a/src/backend/database/db/schema.ts +++ b/src/backend/database/db/schema.ts @@ -508,26 +508,6 @@ export const recentActivity = sqliteTable( ], ); -export const commandHistory = sqliteTable( - "command_history", - { - 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" }), - command: text("command").notNull(), - executedAt: text("executed_at") - .notNull() - .default(sql`CURRENT_TIMESTAMP`), - }, - (table) => [ - index("idx_command_history_user_host").on(table.userId, table.hostId), - ], -); - export const hostAccess = sqliteTable( "host_access", { diff --git a/src/backend/database/repositories/command-history-repository.ts b/src/backend/database/repositories/command-history-repository.ts deleted file mode 100644 index da7b65714..000000000 --- a/src/backend/database/repositories/command-history-repository.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { and, desc, eq, inArray, sql } from "drizzle-orm"; -import { commandHistory } from "../db/schema.js"; -import type { DatabaseContext } from "./database-context.js"; -import { rowsAffected } from "./mutation-result.js"; -import { insertReturning } from "./returning.js"; - -export type CommandHistoryRecord = typeof commandHistory.$inferSelect; - -export class CommandHistoryRepository { - constructor( - private readonly context: DatabaseContext, - private readonly onWrite?: () => void | Promise<void>, - ) {} - - async create( - userId: string, - hostId: number, - command: string, - executedAt = new Date().toISOString(), - ): Promise<CommandHistoryRecord> { - const [created] = await insertReturning(this.context, commandHistory, { - userId, - hostId, - command, - executedAt, - }); - await this.afterWrite(); - return created; - } - - async listUniqueCommandsForHost( - userId: string, - hostId: number, - limit = 500, - ): Promise<string[]> { - const rows = await this.context.drizzle - .select({ - command: commandHistory.command, - maxExecutedAt: sql<number>`MAX(${commandHistory.executedAt})`, - }) - .from(commandHistory) - .where( - and( - eq(commandHistory.userId, userId), - eq(commandHistory.hostId, hostId), - ), - ) - .groupBy(commandHistory.command) - .orderBy(desc(sql`MAX(${commandHistory.executedAt})`)) - .limit(limit); - - return rows.map((row) => row.command); - } - - async listCommandsForHost( - userId: string, - hostId: number, - limit = 200, - ): Promise<string[]> { - const rows = await this.context.drizzle - .select({ - id: commandHistory.id, - command: commandHistory.command, - }) - .from(commandHistory) - .where( - and( - eq(commandHistory.userId, userId), - eq(commandHistory.hostId, hostId), - ), - ) - .orderBy(desc(commandHistory.executedAt)) - .limit(limit); - - return rows.map((row) => row.command); - } - - async deleteCommandForHost( - userId: string, - hostId: number, - command: string, - ): Promise<number> { - const result = await this.context.drizzle - .delete(commandHistory) - .where( - and( - eq(commandHistory.userId, userId), - eq(commandHistory.hostId, hostId), - eq(commandHistory.command, command), - ), - ); - - if (rowsAffected(result) > 0) { - await this.afterWrite(); - } - - return rowsAffected(result); - } - - async deleteByUserAndHost(userId: string, hostId: number): Promise<number> { - const result = await this.context.drizzle - .delete(commandHistory) - .where( - and( - eq(commandHistory.userId, userId), - eq(commandHistory.hostId, hostId), - ), - ); - - if (rowsAffected(result) > 0) { - await this.afterWrite(); - } - - return rowsAffected(result); - } - - async deleteByHostId(hostId: number): Promise<number> { - const result = await this.context.drizzle - .delete(commandHistory) - .where(eq(commandHistory.hostId, hostId)); - - if (rowsAffected(result) > 0) { - await this.afterWrite(); - } - - return rowsAffected(result); - } - - async deleteByHostIds(hostIds: number[]): Promise<number> { - if (hostIds.length === 0) { - return 0; - } - - const result = await this.context.drizzle - .delete(commandHistory) - .where(inArray(commandHistory.hostId, hostIds)); - - if (rowsAffected(result) > 0) { - await this.afterWrite(); - } - - return rowsAffected(result); - } - - async deleteByUserId(userId: string): Promise<number> { - const result = await this.context.drizzle - .delete(commandHistory) - .where(eq(commandHistory.userId, userId)); - - if (rowsAffected(result) > 0) { - await this.afterWrite(); - } - - return rowsAffected(result); - } - - private async afterWrite(): Promise<void> { - await this.onWrite?.(); - } -} diff --git a/src/backend/database/repositories/factory.ts b/src/backend/database/repositories/factory.ts index 56dd8ccfc..d191374fe 100644 --- a/src/backend/database/repositories/factory.ts +++ b/src/backend/database/repositories/factory.ts @@ -9,7 +9,6 @@ import { NotificationChannelRepository } from "./notification-channel-repository import { AutomationRepository } from "./automation-repository.js"; import { ApiKeyRepository } from "./api-key-repository.js"; import { AuditLogRepository } from "./audit-log-repository.js"; -import { CommandHistoryRepository } from "./command-history-repository.js"; import { CredentialRepository } from "./credential-repository.js"; import { DashboardServiceLinkRepository } from "./dashboard-service-link-repository.js"; import { DismissedAlertRepository } from "./dismissed-alert-repository.js"; @@ -184,13 +183,6 @@ export function createCurrentAuditLogRepository(): AuditLogRepository { ); } -export function createCurrentCommandHistoryRepository(): CommandHistoryRepository { - return new CommandHistoryRepository( - createCurrentRepositoryContext(), - createCurrentRepositoryWriteHook("command_history_repository_write"), - ); -} - export function createCurrentCredentialRepository(): CredentialRepository { return new CredentialRepository( createCurrentRepositoryContext(), diff --git a/src/backend/database/routes/delete-user-data.ts b/src/backend/database/routes/delete-user-data.ts index fc43e9275..400e886f1 100644 --- a/src/backend/database/routes/delete-user-data.ts +++ b/src/backend/database/routes/delete-user-data.ts @@ -4,7 +4,6 @@ import { createCurrentNotificationChannelRepository, createCurrentApiKeyRepository, createCurrentAuditLogRepository, - createCurrentCommandHistoryRepository, createCurrentCredentialRepository, createCurrentDashboardServiceLinkRepository, createCurrentDismissedAlertRepository, @@ -94,8 +93,6 @@ export async function deleteUserAndRelatedData( await createCurrentHostFolderRepository().deleteByUserId(userId); - await createCurrentCommandHistoryRepository().deleteByUserId(userId); - await createCurrentHostHealthRepository().deleteByUserId(userId); await createCurrentHostMetricsPreferenceRepository().deleteByUserId(userId); await createCurrentHostSidebarPreferenceRepository().deleteByUserId(userId); diff --git a/src/backend/database/routes/host-command-history-routes.ts b/src/backend/database/routes/host-command-history-routes.ts deleted file mode 100644 index 5658e6f66..000000000 --- a/src/backend/database/routes/host-command-history-routes.ts +++ /dev/null @@ -1,139 +0,0 @@ -import type { AuthenticatedRequest } from "../../../types/index.js"; -import type { Request, RequestHandler, Response, Router } from "express"; -import { sshLogger } from "../../utils/logger.js"; -import { createCurrentCommandHistoryRepository } from "../repositories/factory.js"; -import { isNonEmptyString } from "./host-normalizers.js"; - -export function registerHostCommandHistoryRoutes( - router: Router, - authenticateJWT: RequestHandler, - requireViewPermission: RequestHandler, - requireDataAccess: RequestHandler, -): void { - /** - * @openapi - * /host/command-history/{hostId}: - * get: - * summary: Get command history - * description: Retrieves the command history for a specific host. - * tags: - * - SSH - * parameters: - * - in: path - * name: hostId - * required: true - * schema: - * type: integer - * responses: - * 200: - * description: A list of commands. - * 400: - * description: Invalid userId or hostId. - * 500: - * description: Failed to fetch command history. - */ - router.get( - "/command-history/:hostId", - authenticateJWT, - requireViewPermission, - requireDataAccess, - async (req: Request, res: Response) => { - const userId = (req as AuthenticatedRequest).userId; - const hostIdParam = Array.isArray(req.params.hostId) - ? req.params.hostId[0] - : req.params.hostId; - const hostId = parseInt(hostIdParam, 10); - - if (!isNonEmptyString(userId) || !hostId) { - sshLogger.warn("Invalid userId or hostId for command history fetch", { - operation: "command_history_fetch", - hostId, - userId, - }); - return res.status(400).json({ error: "Invalid userId or hostId" }); - } - - try { - const history = - await createCurrentCommandHistoryRepository().listCommandsForHost( - userId, - hostId, - ); - - res.json(history); - } catch (err) { - sshLogger.error("Failed to fetch command history from database", err, { - operation: "command_history_fetch", - hostId, - userId, - }); - res.status(500).json({ error: "Failed to fetch command history" }); - } - }, - ); - - /** - * @openapi - * /host/command-history: - * delete: - * summary: Delete command from history - * description: Deletes a specific command from the history of a host. - * tags: - * - SSH - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * hostId: - * type: integer - * command: - * type: string - * responses: - * 200: - * description: Command deleted from history. - * 400: - * description: Invalid data. - * 500: - * description: Failed to delete command. - */ - router.delete( - "/command-history", - authenticateJWT, - requireViewPermission, - requireDataAccess, - async (req: Request, res: Response) => { - const userId = (req as AuthenticatedRequest).userId; - const { hostId, command } = req.body; - - if (!isNonEmptyString(userId) || !hostId || !command) { - sshLogger.warn("Invalid data for command history deletion", { - operation: "command_history_delete", - hostId, - userId, - }); - return res.status(400).json({ error: "Invalid data" }); - } - - try { - await createCurrentCommandHistoryRepository().deleteCommandForHost( - userId, - hostId, - command, - ); - - res.json({ message: "Command deleted from history" }); - } catch (err) { - sshLogger.error("Failed to delete command from history", err, { - operation: "command_history_delete", - hostId, - userId, - command, - }); - res.status(500).json({ error: "Failed to delete command" }); - } - }, - ); -} diff --git a/src/backend/database/routes/host-folder-routes.ts b/src/backend/database/routes/host-folder-routes.ts index 3b0521568..486e3cfef 100644 --- a/src/backend/database/routes/host-folder-routes.ts +++ b/src/backend/database/routes/host-folder-routes.ts @@ -3,7 +3,6 @@ import type { AuthenticatedRequest } from "../../../types/index.js"; import { databaseLogger, sshLogger } from "../../utils/logger.js"; import { pluginEvents, TOPICS } from "../../plugins/events.js"; import { - createCurrentCommandHistoryRepository, createCurrentCredentialRepository, createCurrentHostFolderRepository, createCurrentFolderAccessRepository, @@ -398,13 +397,9 @@ export function registerHostFolderRoutes( const hostIds = hostsToDelete.map((host) => host.id); if (hostIds.length > 0) { - // file manager recent/pinned/shortcuts and transfer_recent cascade - // on the host's refHost() foreign key, as the file-manager - // plugin's adopted tables. - - await createCurrentCommandHistoryRepository().deleteByHostIds( - hostIds, - ); + // file manager recent/pinned/shortcuts, transfer_recent and command + // history cascade on the host's refHost() foreign key, as the + // file-manager and ssh-terminal plugins' adopted tables. await createCurrentSshCredentialUsageRepository().deleteByHostIds( hostIds, diff --git a/src/backend/database/routes/host.ts b/src/backend/database/routes/host.ts index ac897e13c..f03b8555d 100644 --- a/src/backend/database/routes/host.ts +++ b/src/backend/database/routes/host.ts @@ -16,7 +16,6 @@ import { } from "../../hosts/credential-username.js"; import { notifyAutomationInternalEvent } from "../../hosts/automation-events.js"; import { - createCurrentCommandHistoryRepository, createCurrentCredentialRepository, createCurrentOpksshTokenRepository, createCurrentRecentActivityRepository, @@ -53,7 +52,6 @@ import { validateParentHostId } from "./host-parent-validation.js"; import { registerHostOpksshRoutes } from "./host-opkssh-routes.js"; import { registerHostStepCaRoutes } from "./host-step-ca-routes.js"; import { registerHostFolderRoutes } from "./host-folder-routes.js"; -import { registerHostCommandHistoryRoutes } from "./host-command-history-routes.js"; import { registerHostAutostartRoutes } from "./host-autostart-routes.js"; import { registerHostInternalRoutes } from "./host-internal-routes.js"; import { registerHostNetworkRoutes } from "./host-network-routes.js"; @@ -2530,13 +2528,9 @@ router.delete( const numericHostId = Number(hostId); - // file manager recent/pinned/shortcuts and transfer_recent cascade on - // the host's refHost() foreign key, as the file-manager plugin's - // adopted tables. - - await createCurrentCommandHistoryRepository().deleteByHostId( - numericHostId, - ); + // file manager recent/pinned/shortcuts, transfer_recent and command + // history cascade on the host's refHost() foreign key, as the + // file-manager and ssh-terminal plugins' adopted tables. await createCurrentSshCredentialUsageRepository().deleteByHostId( numericHostId, @@ -2612,13 +2606,8 @@ router.delete( ); // File manager recent/pinned/shortcuts and transfer/recent routes moved to -// the file-manager plugin, under /plugin-api/file-manager/. -registerHostCommandHistoryRoutes( - router, - authenticateJWT, - permissionManager.requirePermission("hosts.view"), - requireDataAccess, -); +// the file-manager plugin, under /plugin-api/file-manager/, and command +// history to the ssh-terminal plugin, under /plugin-api/ssh-terminal/. async function resolveHostCredentials( host: Record<string, unknown>, diff --git a/src/backend/database/routes/open-tabs.ts b/src/backend/database/routes/open-tabs.ts index 070e5f4a1..481eabc99 100644 --- a/src/backend/database/routes/open-tabs.ts +++ b/src/backend/database/routes/open-tabs.ts @@ -2,9 +2,8 @@ import type { AuthenticatedRequest } from "../../../types/index.js"; import express, { type Request, type Response } from "express"; import { databaseLogger } from "../../utils/logger.js"; import { AuthManager } from "../../utils/auth-manager.js"; -import { sessionManager } from "../../hosts/terminal/session-manager.js"; +import { liveTerminalSessions } from "../../hosts/live-terminal-sessions.js"; import { - getCurrentSettingValue, createCurrentOpenTabRepository, createCurrentSessionShareRepository, } from "../repositories/factory.js"; @@ -26,17 +25,10 @@ const authenticateJWT = authManager.createAuthMiddleware(); */ const DEFAULT_TAB_TTL_MINUTES = 30; +// Tabs live as long as the terminal keeps a detached session. function getTabTtlMs(): number { - try { - const value = getCurrentSettingValue("terminal_session_timeout_minutes"); - if (value) { - const minutes = parseInt(value, 10); - if (!isNaN(minutes) && minutes > 0) return minutes * 60_000; - } - } catch { - // DB not available, use default - } - return DEFAULT_TAB_TTL_MINUTES * 60_000; + const minutes = liveTerminalSessions.idleTimeoutMinutes(); + return (minutes && minutes > 0 ? minutes : DEFAULT_TAB_TTL_MINUTES) * 60_000; } // Legacy tab types that were renamed. Normalize on read so previously saved @@ -273,6 +265,33 @@ router.delete("/:id", authenticateJWT, async (req: Request, res: Response) => { } }); +/** + * @openapi + * /open-tabs/session-timeout: + * get: + * summary: Get how long a detached session is kept + * description: The terminal's idle timeout for detached sessions, which is also how long a saved open tab lives. 30 while the terminal is off. + * tags: + * - Open Tabs + * responses: + * 200: + * description: The timeout in minutes. + * content: + * application/json: + * schema: + * type: object + * properties: + * minutes: + * type: integer + */ +router.get( + "/session-timeout", + authenticateJWT, + (_req: Request, res: Response) => { + res.json({ minutes: getTabTtlMs() / 60_000 }); + }, +); + /** * @openapi * /open-tabs/active-sessions: @@ -324,12 +343,12 @@ router.get( async (req: Request, res: Response) => { const userId = (req as AuthenticatedRequest).userId; try { - const ownSessions = sessionManager.getUserSessions(userId); + const ownSessions = liveTerminalSessions.listForUser(userId); const result = ownSessions.map((s) => ({ sessionId: s.id, hostId: s.hostId, hostName: s.hostName, - tabInstanceId: s.attachedTabInstanceId ?? s.tabInstanceId ?? null, + tabInstanceId: s.tabInstanceId, isConnected: s.isConnected, createdAt: s.createdAt, isOwnSession: true, @@ -344,16 +363,13 @@ router.get( ); for (const share of sharedWithMe) { if (share.protocol !== "ssh") continue; - const sharedSession = sessionManager.getSession(share.sessionId); + const sharedSession = liveTerminalSessions.getSession(share.sessionId); if (!sharedSession || !sharedSession.isConnected) continue; result.push({ sessionId: sharedSession.id, hostId: sharedSession.hostId, hostName: sharedSession.hostName, - tabInstanceId: - sharedSession.attachedTabInstanceId ?? - sharedSession.tabInstanceId ?? - null, + tabInstanceId: sharedSession.tabInstanceId, isConnected: sharedSession.isConnected, createdAt: sharedSession.createdAt, isOwnSession: false, diff --git a/src/backend/database/routes/terminal.ts b/src/backend/database/routes/terminal.ts deleted file mode 100644 index d80b668f5..000000000 --- a/src/backend/database/routes/terminal.ts +++ /dev/null @@ -1,778 +0,0 @@ -import { getErrorMessage } from "../../utils/error-message.js"; -import type { AuthenticatedRequest } from "../../../types/index.js"; -import express, { - type NextFunction, - type Request, - type Response, -} from "express"; -import { randomUUID } from "crypto"; -import multer from "multer"; -import { - createConcurrencyLimiter, - exceedsNormalizedImageSize, - imageExtensionForFormat, -} from "./terminal-image-utils.js"; -import { resolveTerminalImageStorageSettings } from "./terminal-image-storage-settings.js"; -import { - selectImageStorageMode, - probeLocalImageVisibility, - storeImageLocally, - storeImageViaSftp, - TerminalImageStorageError, - type ImageSftpClient, -} from "./terminal-image-storage.js"; -import { authLogger, databaseLogger } from "../../utils/logger.js"; -import { AuthManager } from "../../utils/auth-manager.js"; -import { sessionManager } from "../../hosts/terminal/session-manager.js"; -import { - createCurrentCommandHistoryRepository, - createCurrentHostResolutionRepository, - createCurrentSettingsRepository, -} from "../repositories/factory.js"; - -const router = express.Router(); - -function isNonEmptyString(val: unknown): val is string { - return typeof val === "string" && val.trim().length > 0; -} - -const authManager = AuthManager.getInstance(); -const authenticateJWT = authManager.createAuthMiddleware(); -const requireDataAccess = authManager.createDataAccessMiddleware(); - -// Browser image handoff for local terminal-agent workflows. -const imageUpload = multer({ - storage: multer.memoryStorage(), - limits: { - fileSize: 50 * 1024 * 1024, - fields: 4, - fieldSize: 64 * 1024, - files: 1, - parts: 5, - headerPairs: 200, - }, -}); -const imageUploadMiddleware = imageUpload.single("image"); - -type SharpFactory = typeof import("sharp").default; - -let sharpFactory: SharpFactory | null = null; -let sharpLoadFailure: string | null = null; - -// A static import of sharp takes backend startup down with it when the native -// binary for the running architecture is missing. Resolve on first use so only -// image upload is lost. -async function loadSharp(): Promise<SharpFactory> { - if (sharpFactory) return sharpFactory; - if (sharpLoadFailure !== null) throw new Error(sharpLoadFailure); - try { - sharpFactory = (await import("sharp")).default; - return sharpFactory; - } catch (error) { - sharpLoadFailure = getErrorMessage(error, "unknown"); - throw new Error(sharpLoadFailure); - } -} - -const imageProcessingLimiter = createConcurrencyLimiter(4, 4); -const imageMultipartAdmissionLimiter = createConcurrencyLimiter(4, 4); -let imageUploadSequence = 0; - -async function handleImageUploadMiddleware( - req: Request, - res: Response, - next: NextFunction, -): Promise<void> { - let releaseAdmission: (() => void) | undefined; - try { - releaseAdmission = await imageMultipartAdmissionLimiter.acquire(); - } catch { - res.status(503).json({ - error: "Image upload capacity is temporarily unavailable", - code: "IMAGE_UPLOAD_CAPACITY_EXCEEDED", - }); - return; - } - - imageUploadMiddleware(req, res, (error: unknown) => { - try { - if (!error) { - next(); - return; - } - if (error instanceof multer.MulterError) { - databaseLogger.warn("Image upload multipart request rejected", { - operation: "terminal_image_upload_multipart_rejected", - code: error.code, - field: error.field, - contentType: req.headers["content-type"]?.split(";", 1)[0], - }); - res.status(400).json({ - error: "Image upload request rejected", - code: error.code, - field: error.field, - }); - return; - } - databaseLogger.warn("Image upload multipart request malformed", { - operation: "terminal_image_upload_multipart_invalid", - contentType: req.headers["content-type"]?.split(";", 1)[0], - }); - res.status(400).json({ - error: "Malformed image upload request", - code: "IMAGE_MULTIPART_INVALID", - }); - } finally { - releaseAdmission?.(); - } - }); -} -function findTerminalSession(userId: string, instanceId: string) { - return sessionManager - .getUserSessions(userId) - .find( - (session) => - (session.attachedTabInstanceId ?? session.tabInstanceId) === - instanceId && session.isConnected, - ); -} - -router.post( - "/image-upload", - authenticateJWT, - requireDataAccess, - handleImageUploadMiddleware, - async (req: Request, res: Response) => { - const userId = (req as AuthenticatedRequest).userId; - const instanceId = req.body?.instanceId; - if (!req.file) { - return res.status(400).json({ - error: "Image required", - code: "IMAGE_FILE_MISSING", - }); - } - if (!isNonEmptyString(userId)) { - return res.status(400).json({ - error: "Missing terminal session", - code: "IMAGE_SESSION_MISSING", - }); - } - - const requestId = randomUUID(); - const sequence = ++imageUploadSequence; - const source = - req.body?.source === "file" || req.body?.source === "clipboard" - ? req.body.source - : undefined; - const clientUploadTimestamp = - typeof req.body?.clientUploadTimestamp === "string" && - !Number.isNaN(Date.parse(req.body.clientUploadTimestamp)) - ? req.body.clientUploadTimestamp - : undefined; - const serverReceivedAt = new Date().toISOString(); - databaseLogger.info("Terminal image upload received", { - operation: "terminal_image_upload_received", - requestId, - sequence, - source, - clientUploadTimestamp, - serverReceivedAt, - bytes: req.file.size, - }); - - const storageSettings = await resolveTerminalImageStorageSettings( - createCurrentSettingsRepository(), - ); - const session = isNonEmptyString(instanceId) - ? findTerminalSession(userId, instanceId) - : undefined; - let localHostVisible = false; - if (storageSettings.localMappingConfigured && session?.sshConn) { - localHostVisible = await probeLocalImageVisibility( - session.sshConn, - storageSettings, - ).catch(() => false); - } - const storageMode = selectImageStorageMode(storageSettings, { - remoteSftpAvailable: !!session?.sshConn, - localHostVisible, - }); - - if (storageMode === "unavailable") { - return res.status(503).json({ - error: "Image storage is unavailable", - code: "IMAGE_STORAGE_UNAVAILABLE", - }); - } - - if (storageMode === "local" && !storageSettings.localMappingConfigured) { - return res.status(503).json({ - error: "Local image storage is not configured", - code: "IMAGE_LOCAL_STORAGE_NOT_CONFIGURED", - }); - } - - if (storageMode === "remote-sftp") { - if (!isNonEmptyString(instanceId)) { - return res.status(400).json({ - error: "Missing terminal session", - code: "IMAGE_SESSION_MISSING", - }); - } - if (!session || !session.sshConn) { - return res.status(409).json({ - error: "Terminal is not connected", - code: "IMAGE_TERMINAL_NOT_CONNECTED", - }); - } - } - - let sharp: SharpFactory; - try { - sharp = await loadSharp(); - } catch (error) { - databaseLogger.error( - "Image processing unavailable: sharp failed to load", - error, - { - operation: "terminal_image_upload_sharp_unavailable", - reason: getErrorMessage(error, "unknown"), - }, - ); - return res.status(503).json({ - error: "Image processing is unavailable on this installation", - code: "IMAGE_PROCESSING_UNAVAILABLE", - }); - } - - let normalizedImage: Buffer; - let releaseImageProcessingSlot: (() => void) | undefined; - try { - releaseImageProcessingSlot = await imageProcessingLimiter.acquire(); - } catch { - return res.status(503).json({ - error: "Image upload capacity is temporarily exhausted", - code: "IMAGE_UPLOAD_CAPACITY_EXCEEDED", - }); - } - try { - const source = sharp(req.file.buffer, { - failOn: "error", - limitInputPixels: 40_000_000, - }); - const { format } = await source.metadata(); - if (!imageExtensionForFormat(format)) { - return res.status(400).json({ - error: "Unsupported image format", - code: "IMAGE_FORMAT_UNSUPPORTED", - }); - } - normalizedImage = await source.rotate().png().toBuffer(); - if (exceedsNormalizedImageSize(normalizedImage.length)) { - return res.status(413).json({ - error: "Normalized image is too large", - code: "IMAGE_NORMALIZED_SIZE_LIMIT", - }); - } - } catch (error) { - databaseLogger.warn("Image upload failed image decoding", { - operation: "terminal_image_upload_decode", - mimeType: req.file.mimetype, - bytes: req.file.size, - reason: getErrorMessage(error, "unknown"), - }); - return res.status(400).json({ - error: "Invalid image data", - code: "IMAGE_DECODE_FAILED", - }); - } finally { - releaseImageProcessingSlot?.(); - } - - let remoteSftp: ImageSftpClient | undefined; - try { - const stored = - storageMode === "remote-sftp" - ? await (async () => { - remoteSftp = await new Promise<ImageSftpClient>( - (resolve, reject) => { - let settled = false; - const timer = setTimeout(() => { - settled = true; - reject(new Error("SFTP channel acquisition timed out")); - }, 3_000); - session!.sshConn!.sftp((err, sftp) => { - if (settled) { - sftp?.end?.(); - return; - } - settled = true; - clearTimeout(timer); - if (err) return reject(err); - resolve(sftp); - }); - }, - ); - return storeImageViaSftp(remoteSftp, normalizedImage, { - ttlMs: storageSettings.ttlMs, - maxCount: storageSettings.maxCount, - maxBytes: storageSettings.maxBytes, - }); - })() - : await storeImageLocally(normalizedImage, storageSettings); - - res.json(stored); - } catch (error) { - if (error instanceof TerminalImageStorageError) { - const status = - error.code === "IMAGE_STORAGE_LIMIT_REACHED" - ? 507 - : error.code === "IMAGE_REMOTE_WRITE_FAILED" - ? 502 - : error.code === "IMAGE_REMOTE_QUOTA_UNAVAILABLE" - ? 503 - : error.code === "IMAGE_LOCAL_INSPECTION_FAILED" - ? 503 - : 500; - databaseLogger.warn("Image upload storage write failed", { - operation: - error.code === "IMAGE_REMOTE_WRITE_FAILED" - ? "terminal_image_upload_sftp_failed" - : "terminal_image_upload_local_failed", - code: error.code, - userId, - instanceId, - reason: getErrorMessage(error.cause ?? error, "unknown"), - }); - return res.status(status).json({ - error: error.message, - code: error.code, - }); - } - databaseLogger.warn("Image upload failed to acquire remote channel", { - operation: "terminal_image_upload_sftp_failed", - code: "IMAGE_REMOTE_WRITE_FAILED", - userId, - instanceId, - reason: getErrorMessage(error, "unknown"), - }); - return res.status(502).json({ - error: "Failed to write image to the remote host", - code: "IMAGE_REMOTE_WRITE_FAILED", - }); - } finally { - remoteSftp?.end?.(); - } - }, -); - -/** - * @openapi - * /terminal/command_history: - * post: - * summary: Save command to history - * description: Saves a command to the command history for a specific host. - * tags: - * - Terminal - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * hostId: - * type: integer - * command: - * type: string - * responses: - * 201: - * description: Command saved successfully. - * 400: - * description: Missing required parameters. - * 500: - * description: Failed to save command. - */ -router.post( - "/command_history", - authenticateJWT, - requireDataAccess, - async (req: Request, res: Response) => { - const userId = (req as AuthenticatedRequest).userId; - const { hostId, command } = req.body; - - if (!isNonEmptyString(userId) || !hostId || !isNonEmptyString(command)) { - authLogger.warn("Invalid command history save request", { - operation: "command_history_save", - userId, - hasHostId: !!hostId, - hasCommand: !!command, - }); - return res.status(400).json({ error: "Missing required parameters" }); - } - - const sensitivePatterns = [ - /passw(or)?d/i, - /\bsecret\b/i, - /\btoken\b/i, - /\bapi.?key\b/i, - /PASS(WORD)?=/i, - /AWS_SECRET/i, - /mysql\b.*-p/i, - /sudo\s+-S\b/, - /htpasswd/i, - /sshpass/i, - /curl\b.*-u\s/i, - /export\b.*(?:PASSWORD|SECRET|TOKEN|KEY)=/i, - ]; - - const trimmedCommand = command.trim(); - if (sensitivePatterns.some((p: RegExp) => p.test(trimmedCommand))) { - return res.status(201).json({ - id: 0, - userId, - hostId: parseInt(hostId, 10), - command: trimmedCommand, - executedAt: new Date().toISOString(), - }); - } - - const globalEnabled = await createCurrentSettingsRepository().getBoolean( - "command_history_enabled", - true, - ); - if (!globalEnabled) { - return res.status(201).json({ - id: 0, - userId, - hostId: parseInt(hostId, 10), - command: trimmedCommand, - executedAt: new Date().toISOString(), - }); - } - - const hostRecord = - await createCurrentHostResolutionRepository().findHostById( - parseInt(hostId, 10), - userId, - ); - if (hostRecord?.enableCommandHistory === false) { - return res.status(201).json({ - id: 0, - userId, - hostId: parseInt(hostId, 10), - command: trimmedCommand, - executedAt: new Date().toISOString(), - }); - } - - try { - const result = await createCurrentCommandHistoryRepository().create( - userId, - parseInt(hostId, 10), - trimmedCommand, - ); - - res.status(201).json(result); - } catch (err) { - authLogger.error("Failed to save command to history", err); - res.status(500).json({ - error: getErrorMessage(err, "Failed to save command"), - }); - } - }, -); - -/** - * @openapi - * /terminal/command_history/{hostId}: - * get: - * summary: Get command history - * description: Retrieves the command history for a specific host. - * tags: - * - Terminal - * parameters: - * - in: path - * name: hostId - * required: true - * schema: - * type: integer - * responses: - * 200: - * description: A list of commands. - * 400: - * description: Invalid request parameters. - * 500: - * description: Failed to fetch history. - */ -router.get( - "/command_history/:hostId", - authenticateJWT, - requireDataAccess, - async (req: Request, res: Response) => { - const userId = (req as AuthenticatedRequest).userId; - const hostId = Array.isArray(req.params.hostId) - ? req.params.hostId[0] - : req.params.hostId; - const hostIdNum = parseInt(hostId, 10); - - if (!isNonEmptyString(userId) || isNaN(hostIdNum)) { - authLogger.warn("Invalid command history fetch request", { - userId, - hostId: hostIdNum, - }); - return res.status(400).json({ error: "Invalid request parameters" }); - } - - try { - const uniqueCommands = - await createCurrentCommandHistoryRepository().listUniqueCommandsForHost( - userId, - hostIdNum, - ); - - res.json(uniqueCommands); - } catch (err) { - authLogger.error("Failed to fetch command history", err); - res.status(500).json({ - error: getErrorMessage(err, "Failed to fetch history"), - }); - } - }, -); - -/** - * @openapi - * /terminal/command_history/delete: - * post: - * summary: Delete a specific command from history - * description: Deletes a specific command from the history of a host. - * tags: - * - Terminal - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * hostId: - * type: integer - * command: - * type: string - * responses: - * 200: - * description: Command deleted successfully. - * 400: - * description: Missing required parameters. - * 500: - * description: Failed to delete command. - */ -router.post( - "/command_history/delete", - authenticateJWT, - requireDataAccess, - async (req: Request, res: Response) => { - const userId = (req as AuthenticatedRequest).userId; - const { hostId, command } = req.body; - - if (!isNonEmptyString(userId) || !hostId || !isNonEmptyString(command)) { - authLogger.warn("Invalid command delete request", { - operation: "command_history_delete", - userId, - hasHostId: !!hostId, - hasCommand: !!command, - }); - return res.status(400).json({ error: "Missing required parameters" }); - } - - try { - const hostIdNum = parseInt(hostId, 10); - - await createCurrentCommandHistoryRepository().deleteCommandForHost( - userId, - hostIdNum, - command.trim(), - ); - - res.json({ success: true }); - } catch (err) { - authLogger.error("Failed to delete command from history", err); - res.status(500).json({ - error: getErrorMessage(err, "Failed to delete command"), - }); - } - }, -); - -/** - * @openapi - * /terminal/command_history/{hostId}: - * delete: - * summary: Clear command history - * description: Clears the entire command history for a specific host. - * tags: - * - Terminal - * parameters: - * - in: path - * name: hostId - * required: true - * schema: - * type: integer - * responses: - * 200: - * description: Command history cleared successfully. - * 400: - * description: Invalid request. - * 500: - * description: Failed to clear history. - */ -router.delete( - "/command_history/:hostId", - authenticateJWT, - requireDataAccess, - async (req: Request, res: Response) => { - const userId = (req as AuthenticatedRequest).userId; - const hostId = Array.isArray(req.params.hostId) - ? req.params.hostId[0] - : req.params.hostId; - const hostIdNum = parseInt(hostId, 10); - - if (!isNonEmptyString(userId) || isNaN(hostIdNum)) { - authLogger.warn("Invalid command history clear request"); - return res.status(400).json({ error: "Invalid request" }); - } - - try { - await createCurrentCommandHistoryRepository().deleteByUserAndHost( - userId, - hostIdNum, - ); - databaseLogger.info("Terminal history cleared", { - operation: "terminal_history_clear", - userId, - hostId: hostIdNum, - }); - - res.json({ success: true }); - } catch (err) { - authLogger.error("Failed to clear command history", err); - res.status(500).json({ - error: getErrorMessage(err, "Failed to clear history"), - }); - } - }, -); - -/** - * @openapi - * /terminal/session_settings: - * get: - * summary: Get session persistence settings - * description: Returns the session timeout and persistence enabled flag. - * tags: - * - Terminal - * responses: - * 200: - * description: Session settings. - * 500: - * description: Failed to fetch settings. - */ -router.get( - "/session_settings", - authenticateJWT, - async (_req: Request, res: Response) => { - try { - const settings = createCurrentSettingsRepository(); - const timeoutValue = await settings.get( - "terminal_session_timeout_minutes", - ); - const enabled = await settings.getBoolean( - "terminal_session_persistence_enabled", - true, - ); - - res.json({ - timeoutMinutes: timeoutValue ? parseInt(timeoutValue, 10) : 30, - enabled, - }); - } catch (err) { - authLogger.error("Failed to fetch session settings", err); - res.status(500).json({ - error: getErrorMessage(err, "Failed to fetch settings"), - }); - } - }, -); - -/** - * @openapi - * /terminal/session_settings: - * post: - * summary: Update session persistence settings - * description: Saves session timeout and persistence enabled flag. - * tags: - * - Terminal - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * timeoutMinutes: - * type: integer - * enabled: - * type: boolean - * responses: - * 200: - * description: Settings saved successfully. - * 400: - * description: Invalid parameters. - * 500: - * description: Failed to save settings. - */ -router.post( - "/session_settings", - authenticateJWT, - async (req: Request, res: Response) => { - const { timeoutMinutes, enabled } = req.body; - - if ( - timeoutMinutes !== undefined && - (typeof timeoutMinutes !== "number" || - timeoutMinutes < 1 || - timeoutMinutes > 1440) - ) { - return res - .status(400) - .json({ error: "timeoutMinutes must be between 1 and 1440" }); - } - - try { - const settings = createCurrentSettingsRepository(); - if (timeoutMinutes !== undefined) { - await settings.set( - "terminal_session_timeout_minutes", - String(timeoutMinutes), - ); - } - - if (enabled !== undefined) { - await settings.set( - "terminal_session_persistence_enabled", - String(enabled), - ); - } - - res.json({ success: true }); - } catch (err) { - authLogger.error("Failed to save session settings", err); - res.status(500).json({ - error: getErrorMessage(err, "Failed to save settings"), - }); - } - }, -); - -export default router; diff --git a/src/backend/database/routes/touch-input-settings-routes.ts b/src/backend/database/routes/touch-input-settings-routes.ts deleted file mode 100644 index 4a78617b2..000000000 --- a/src/backend/database/routes/touch-input-settings-routes.ts +++ /dev/null @@ -1,69 +0,0 @@ -import type { RequestHandler, Router } from "express"; -import type { AuthenticatedRequest } from "../../../types/index.js"; -import { - normalizeTouchInputSettings, - TOUCH_INPUT_SETTING_KEY, - validateTouchInputSettingsUpdate, -} from "../../../types/touch-input-settings.js"; -import { authLogger } from "../../utils/logger.js"; -import { - createCurrentSettingsRepository, - createCurrentUserRepository, -} from "../repositories/factory.js"; - -async function readSettings() { - const raw = await createCurrentSettingsRepository().get( - TOUCH_INPUT_SETTING_KEY, - ); - if (!raw) return normalizeTouchInputSettings(null); - try { - return normalizeTouchInputSettings(JSON.parse(raw)); - } catch { - return normalizeTouchInputSettings(null); - } -} - -export function registerTouchInputSettingsRoutes( - router: Router, - authenticateJWT: RequestHandler, -): void { - router.get("/touch-input-settings", authenticateJWT, async (_req, res) => { - try { - res.json(await readSettings()); - } catch (error) { - authLogger.error("Failed to get touch input settings", error); - res.status(500).json({ error: "Failed to get touch input settings" }); - } - }); - - router.patch("/touch-input-settings", authenticateJWT, async (req, res) => { - const userId = (req as AuthenticatedRequest).userId; - try { - const user = userId - ? await createCurrentUserRepository().findById(userId) - : null; - if (!user?.isAdmin) { - return res.status(403).json({ error: "Not authorized" }); - } - const validationError = validateTouchInputSettingsUpdate(req.body); - if (validationError) { - return res.status(400).json({ error: validationError }); - } - const current = await readSettings(); - const merged = { ...current, ...req.body }; - const mergedValidationError = validateTouchInputSettingsUpdate(merged); - if (mergedValidationError) { - return res.status(400).json({ error: mergedValidationError }); - } - const next = normalizeTouchInputSettings(merged); - await createCurrentSettingsRepository().set( - TOUCH_INPUT_SETTING_KEY, - JSON.stringify(next), - ); - res.json(next); - } catch (error) { - authLogger.error("Failed to update touch input settings", error); - res.status(500).json({ error: "Failed to update touch input settings" }); - } - }); -} diff --git a/src/backend/database/routes/user-image-storage-routes.ts b/src/backend/database/routes/user-image-storage-routes.ts deleted file mode 100644 index 4b7eacff7..000000000 --- a/src/backend/database/routes/user-image-storage-routes.ts +++ /dev/null @@ -1,334 +0,0 @@ -import type { Request, RequestHandler, Response, Router } from "express"; -import type { AuthenticatedRequest } from "../../../types/index.js"; -import { databaseLogger } from "../../utils/logger.js"; -import { sessionManager } from "../../hosts/terminal/session-manager.js"; -import { createCurrentSettingsRepository } from "../repositories/factory.js"; -import { - MIN_IMAGE_MAX_BYTES, - TERMINAL_IMAGE_STORAGE_KEYS, - parseImageHostPath, - parseImageLocalDir, - parseTerminalImageStorageMode, - resolveTerminalImageStorageSettings, - type TerminalImageStorageSettings, -} from "./terminal-image-storage-settings.js"; -import { - probeLocalImageVisibility, - selectImageStorageMode, -} from "./terminal-image-storage.js"; - -/** - * Admin-only terminal image storage settings. - * - * The public shape deliberately omits `localDir`: it is a backend-internal - * path and only the agent-visible `hostPath` may leave the server. No - * credentials or connection details are ever returned here. - */ -interface PublicImageStorageSettings { - mode: TerminalImageStorageSettings["mode"]; - hostPath: string; - ttlMs: number; - maxCount: number; - maxBytes: number; - localMappingConfigured: boolean; -} - -function toPublicSettings( - settings: TerminalImageStorageSettings, -): PublicImageStorageSettings { - return { - mode: settings.mode, - hostPath: settings.hostPath, - ttlMs: settings.ttlMs, - maxCount: settings.maxCount, - maxBytes: settings.maxBytes, - localMappingConfigured: settings.localMappingConfigured, - }; -} - -const PATCHABLE_FIELDS = [ - "mode", - "localDir", - "hostPath", - "ttlMs", - "maxCount", - "maxBytes", -] as const; - -type PatchableField = (typeof PATCHABLE_FIELDS)[number]; - -function invalidField(res: Response, field: string): void { - // Safe by construction: the field name is fixed, the rejected value and - // any backend path are never echoed back. - res.status(400).json({ - error: `Invalid value for ${field}`, - code: "IMAGE_STORAGE_SETTINGS_INVALID", - field, - }); -} - -function parseIntegerField(value: unknown, min: number): number | null { - if (typeof value !== "number" || !Number.isInteger(value)) return null; - if (value < min) return null; - return value; -} - -/** - * Validates a PATCH body and returns the settings-table writes it implies, or - * null after the response has already been failed with a 400. - */ -function buildImageStorageWrites( - body: unknown, - res: Response, -): Array<{ key: string; value: string }> | null { - if (typeof body !== "object" || body === null || Array.isArray(body)) { - res.status(400).json({ - error: "Invalid request body", - code: "IMAGE_STORAGE_SETTINGS_INVALID", - }); - return null; - } - - for (const field of Object.keys(body)) { - if (!(PATCHABLE_FIELDS as readonly string[]).includes(field)) { - res.status(400).json({ - error: `Unknown setting field: ${field}`, - code: "IMAGE_STORAGE_SETTINGS_UNKNOWN_FIELD", - field, - }); - return null; - } - } - - const input = body as Partial<Record<PatchableField, unknown>>; - const writes: Array<{ key: string; value: string }> = []; - - if (input.mode !== undefined) { - const mode = parseTerminalImageStorageMode(input.mode); - if (mode === null) { - invalidField(res, "mode"); - return null; - } - writes.push({ key: TERMINAL_IMAGE_STORAGE_KEYS.mode, value: mode }); - } - - if (input.localDir !== undefined) { - const localDir = parseImageLocalDir(input.localDir); - if (localDir === null) { - invalidField(res, "localDir"); - return null; - } - writes.push({ key: TERMINAL_IMAGE_STORAGE_KEYS.localDir, value: localDir }); - } - - if (input.hostPath !== undefined) { - const hostPath = parseImageHostPath(input.hostPath); - if (hostPath === null) { - invalidField(res, "hostPath"); - return null; - } - writes.push({ - key: TERMINAL_IMAGE_STORAGE_KEYS.hostPath, - value: hostPath, - }); - } - - if (input.ttlMs !== undefined) { - const ttlMs = parseIntegerField(input.ttlMs, 0); - if (ttlMs === null) { - invalidField(res, "ttlMs"); - return null; - } - writes.push({ - key: TERMINAL_IMAGE_STORAGE_KEYS.ttlMs, - value: String(ttlMs), - }); - } - - if (input.maxCount !== undefined) { - const maxCount = parseIntegerField(input.maxCount, 1); - if (maxCount === null) { - invalidField(res, "maxCount"); - return null; - } - writes.push({ - key: TERMINAL_IMAGE_STORAGE_KEYS.maxCount, - value: String(maxCount), - }); - } - - if (input.maxBytes !== undefined) { - const maxBytes = parseIntegerField(input.maxBytes, MIN_IMAGE_MAX_BYTES); - if (maxBytes === null) { - invalidField(res, "maxBytes"); - return null; - } - writes.push({ - key: TERMINAL_IMAGE_STORAGE_KEYS.maxBytes, - value: String(maxBytes), - }); - } - - return writes; -} - -export function registerUserImageStorageRoutes( - router: Router, - requireAdmin: RequestHandler, -): void { - /** - * @openapi - * /users/terminal-image-storage-settings: - * get: - * summary: Get terminal image storage settings (admin only) - * description: Returns the effective terminal image storage settings. The backend-internal localDir is never exposed; only the agent-visible hostPath is returned. - * tags: - * - Users - * responses: - * 200: - * description: Effective image storage settings. - * 401: - * description: Not authenticated. - * 403: - * description: Admin access required. - * 500: - * description: Failed to load settings. - */ - router.get( - "/terminal-image-storage-settings", - requireAdmin, - async (_req: Request, res: Response) => { - try { - const settings = await resolveTerminalImageStorageSettings( - createCurrentSettingsRepository(), - ); - res.json(toPublicSettings(settings)); - } catch (err) { - databaseLogger.error("Failed to load image storage settings", err); - res - .status(500) - .json({ error: "Failed to load image storage settings" }); - } - }, - ); - - /** - * @openapi - * /users/terminal-image-storage-settings: - * patch: - * summary: Update terminal image storage settings (admin only) - * description: Persists a partial update. Accepts only mode (auto, local, remote-sftp), localDir, hostPath, ttlMs, maxCount and maxBytes; invalid values are rejected with a 400. - * tags: - * - Users - * responses: - * 200: - * description: Updated effective settings. - * 400: - * description: Invalid or unknown setting field. - * 401: - * description: Not authenticated. - * 403: - * description: Admin access required. - * 500: - * description: Failed to save settings. - */ - router.patch( - "/terminal-image-storage-settings", - requireAdmin, - async (req: Request, res: Response) => { - const writes = buildImageStorageWrites(req.body, res); - if (writes === null) return; - - try { - const settings = createCurrentSettingsRepository(); - if (typeof settings.setMany !== "function") { - throw new Error("Atomic settings persistence is unavailable"); - } - await settings.setMany(writes); - const resolved = await resolveTerminalImageStorageSettings(settings); - res.json(toPublicSettings(resolved)); - } catch (err) { - databaseLogger.error("Failed to save image storage settings", err); - res - .status(500) - .json({ error: "Failed to save image storage settings" }); - } - }, - ); - - /** - * @openapi - * /users/terminal-image-storage-settings/test: - * post: - * summary: Test image storage visibility (admin only) - * description: Reports which storage mode an upload would take for one of the caller's already-connected terminal sessions. Uses the bounded local-mapping probe only; it never opens new connections. - * tags: - * - Users - * responses: - * 200: - * description: Visibility test result. - * 400: - * description: Missing terminal session instanceId. - * 401: - * description: Not authenticated. - * 403: - * description: Admin access required. - * 500: - * description: Failed to run the test. - */ - router.post( - "/terminal-image-storage-settings/test", - requireAdmin, - async (req: Request, res: Response) => { - const userId = (req as AuthenticatedRequest).userId; - const instanceId = req.body?.instanceId; - if (typeof instanceId !== "string" || instanceId.trim().length === 0) { - return res.status(400).json({ - error: "Missing terminal session", - code: "IMAGE_SESSION_MISSING", - }); - } - - try { - const settings = await resolveTerminalImageStorageSettings( - createCurrentSettingsRepository(), - ); - const session = sessionManager - .getUserSessions(userId) - .find( - (candidate) => - (candidate.attachedTabInstanceId ?? candidate.tabInstanceId) === - instanceId && candidate.isConnected, - ); - const remoteSftpAvailable = !!session?.sshConn; - - let localHostVisible: boolean | null = null; - if (settings.localMappingConfigured && session?.sshConn) { - localHostVisible = await probeLocalImageVisibility( - session.sshConn, - settings, - ).catch(() => false); - } - - const selectedMode = selectImageStorageMode(settings, { - remoteSftpAvailable, - ...(localHostVisible !== null ? { localHostVisible } : {}), - }); - - res.json({ - mode: settings.mode, - connected: !!session, - remoteSftpAvailable, - localHostVisible, - selectedMode, - localMappingConfigured: settings.localMappingConfigured, - }); - } catch (err) { - databaseLogger.error("Failed to test image storage visibility", err); - res - .status(500) - .json({ error: "Failed to test image storage visibility" }); - } - }, - ); -} diff --git a/src/backend/database/routes/user-settings-routes.ts b/src/backend/database/routes/user-settings-routes.ts index 1f0483ad2..5474c5696 100644 --- a/src/backend/database/routes/user-settings-routes.ts +++ b/src/backend/database/routes/user-settings-routes.ts @@ -338,110 +338,6 @@ export function registerUserSettingsRoutes( } }); - /** - * @openapi - * /users/command-history-enabled: - * get: - * summary: Get command history enabled setting - * description: Returns whether command history recording is globally enabled. - * tags: - * - Users - * responses: - * 200: - * description: Command history enabled status. - * content: - * application/json: - * schema: - * type: object - * properties: - * enabled: - * type: boolean - */ - router.get("/command-history-enabled", authenticateJWT, async (_req, res) => { - try { - res.json({ - enabled: await createCurrentSettingsRepository().getBoolean( - "command_history_enabled", - true, - ), - }); - } catch (err) { - authLogger.error("Failed to get command history enabled setting", err); - res - .status(500) - .json({ error: "Failed to get command history enabled setting" }); - } - }); - - /** - * @openapi - * /users/command-history-enabled: - * patch: - * summary: Update command history enabled setting (admin only) - * description: Globally enables or disables command history recording for all hosts. - * tags: - * - Users - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * enabled: - * type: boolean - * responses: - * 200: - * description: Setting updated. - * 403: - * description: Not authorized. - * 500: - * description: Failed to update setting. - */ - router.patch( - "/command-history-enabled", - authenticateJWT, - async (req, res) => { - const userId = (req as AuthenticatedRequest).userId; - try { - const actor = await getAdminActor(userId); - if (!actor) { - return res.status(403).json({ error: "Not authorized" }); - } - const { enabled } = req.body; - if (typeof enabled !== "boolean") { - return res.status(400).json({ error: "enabled must be a boolean" }); - } - await createCurrentSettingsRepository().set( - "command_history_enabled", - enabled ? "true" : "false", - ); - - const { ipAddress, userAgent } = getRequestMeta(req); - await logAudit({ - userId, - username: actor.username ?? userId, - action: "update_command_history_enabled", - resourceType: "setting", - details: JSON.stringify({ enabled }), - ipAddress, - userAgent, - success: true, - }); - - res.json({ enabled }); - } catch (err) { - authLogger.error( - "Failed to update command history enabled setting", - err, - ); - res - .status(500) - .json({ error: "Failed to update command history enabled setting" }); - } - }, - ); - /** * @openapi * /users/analytics-enabled: diff --git a/src/backend/database/routes/users.ts b/src/backend/database/routes/users.ts index 893c15791..c07cf3c46 100644 --- a/src/backend/database/routes/users.ts +++ b/src/backend/database/routes/users.ts @@ -24,10 +24,8 @@ import { validateLogoutToken, } from "./user-oidc-utils.js"; import { registerUserApiKeyRoutes } from "./user-api-key-routes.js"; -import { registerUserImageStorageRoutes } from "./user-image-storage-routes.js"; import { registerBrandingRoutes } from "./branding-routes.js"; import { registerUserSettingsRoutes } from "./user-settings-routes.js"; -import { registerTouchInputSettingsRoutes } from "./touch-input-settings-routes.js"; import { registerAcmeSSLRoutes } from "./acme-ssl-routes.js"; import { registerUserTotpRoutes } from "./user-totp-routes.js"; import { registerUserWebAuthnRoutes } from "./user-webauthn-routes.js"; @@ -2126,11 +2124,9 @@ registerUserOidcAccountRoutes(router, { }); registerUserSettingsRoutes(router, authenticateJWT); -registerTouchInputSettingsRoutes(router, authenticateJWT); registerAcmeSSLRoutes(router, authenticateJWT); registerUserApiKeyRoutes(router, requireAdmin); -registerUserImageStorageRoutes(router, requireAdmin); registerBrandingRoutes(router, requireAdmin); registerSSOProviderRoutes(router); diff --git a/src/backend/hosts/collab/routes.ts b/src/backend/hosts/collab/routes.ts index 764369dda..90e104593 100644 --- a/src/backend/hosts/collab/routes.ts +++ b/src/backend/hosts/collab/routes.ts @@ -23,7 +23,7 @@ import { collabRuntimeStore, type CollabControlRequest, } from "./runtime-store.js"; -import { sessionManager } from "../terminal/session-manager.js"; +import { liveTerminalSessions } from "../live-terminal-sessions.js"; import { isLiveSession, isLiveSessionOwnedBy, @@ -79,9 +79,13 @@ async function revokeStageShare(room: CollabRoomRecord): Promise<void> { throw new Error("Failed to revoke the active stage share"); } if (share.protocol === "ssh") { - sessionManager.disconnectShareParticipants(share.sessionId, share.id, { - reason: "The collaboration stage ended", - }); + liveTerminalSessions.disconnectShareParticipants( + share.sessionId, + share.id, + { + reason: "The collaboration stage ended", + }, + ); void collabRuntimeStore.publish(room.id, { type: "collab_internal_stage_revoked", sessionId: share.sessionId, @@ -241,6 +245,8 @@ router.get( controlRequests: mayReviewControlRequests ? controlRequests : controlRequests.filter((request) => request.userId === userId), + // Room events ride on the terminal socket. + eventsWsPath: "/plugin-ws/ssh-terminal/terminal", }); } catch (error) { sshLogger.error("Failed to get collab room", error, { @@ -387,7 +393,7 @@ router.delete( access.room.stageShareId, ); if (share) { - sessionManager.disconnectShareParticipants( + liveTerminalSessions.disconnectShareParticipants( share.sessionId, share.id, { @@ -689,7 +695,7 @@ async function applyStageControl( room.stageShareId, ); if (share) { - sessionManager.setRoomShareControl( + liveTerminalSessions.setRoomShareControl( share.sessionId, share.id, controllerUserId, @@ -722,7 +728,7 @@ collabRuntimeStore.onEvent((roomId, message) => { .findActiveById(room.stageShareId) .then((share) => { if (share) { - sessionManager.setRoomShareControl( + liveTerminalSessions.setRoomShareControl( share.sessionId, share.id, controllerUserId, @@ -739,7 +745,7 @@ collabRuntimeStore.onEvent((roomId, message) => { "shareId" in message && typeof message.shareId === "string" ) { - sessionManager.disconnectShareParticipants( + liveTerminalSessions.disconnectShareParticipants( message.sessionId, message.shareId, { @@ -1016,7 +1022,7 @@ router.post( access.room.stageShareId, ); if (share) { - sessionManager.disconnectShareParticipants( + liveTerminalSessions.disconnectShareParticipants( share.sessionId, share.id, { diff --git a/src/backend/hosts/connect/types.ts b/src/backend/hosts/connect/types.ts index ec06cb178..e0edae1d1 100644 --- a/src/backend/hosts/connect/types.ts +++ b/src/backend/hosts/connect/types.ts @@ -178,6 +178,12 @@ export interface SshAuthProvider { * transports that can show one (the terminal today). */ startInteraction?: (request: SshInteractionRequest) => Promise<void>; + /** Cancels a pending browser step, by request id or by host. */ + cancelInteraction?: (request: { + userId: string; + hostId?: number; + requestId?: string; + }) => void | Promise<void>; prepare: ( config: MutableConnectConfig, host: SshConnectHost, diff --git a/src/backend/hosts/live-terminal-sessions.ts b/src/backend/hosts/live-terminal-sessions.ts new file mode 100644 index 000000000..851d7cc2e --- /dev/null +++ b/src/backend/hosts/live-terminal-sessions.ts @@ -0,0 +1,82 @@ +/** + * What core needs from live SSH terminal sessions, through the plugin + * service "sessions.live" rather than an import. + * + * Session sharing, collab and the open-tabs route still live in core and + * reach the sessions the ssh-terminal plugin holds. With no provider (the + * plugin is off) a lookup finds nothing and a control call does nothing, + * because "no such live session" and "the terminal is off" mean the same + * thing to every caller here. + */ + +import { getServiceImplementation } from "../plugins/service-registry.js"; + +export const LIVE_SESSIONS_SERVICE = "sessions.live"; + +export interface LiveSessionInfo { + id: string; + userId: string; + hostId: number; + hostName: string; + isConnected: boolean; + createdAt: number; + lastDetachedAt: number | null; + tabInstanceId: string | null; + tmuxSessionName: string | null; + cols: number; + rows: number; +} + +interface LiveSessionsV1 { + getSession: (sessionId: string) => LiveSessionInfo | null; + listForUser: (userId: string) => LiveSessionInfo[]; + ownerEndSession: (sessionId: string, reason: string) => void; + disconnectParticipants: ( + sessionId: string, + shareId: string, + options: { reason: string; userId?: string | null }, + ) => number; + setRoomShareControl: ( + sessionId: string, + shareId: string, + controllerUserId: string | null, + ) => void; + idleTimeoutMinutes: () => number; +} + +function provider(): LiveSessionsV1 | undefined { + return getServiceImplementation<LiveSessionsV1>( + LIVE_SESSIONS_SERVICE, + "^1.0.0", + ); +} + +export const liveTerminalSessions = { + getSession(sessionId: string): LiveSessionInfo | null { + return provider()?.getSession(sessionId) ?? null; + }, + listForUser(userId: string): LiveSessionInfo[] { + return provider()?.listForUser(userId) ?? []; + }, + ownerEndSession(sessionId: string, reason: string): void { + provider()?.ownerEndSession(sessionId, reason); + }, + disconnectShareParticipants( + sessionId: string, + shareId: string, + options: { reason: string; userId?: string | null }, + ): number { + return provider()?.disconnectParticipants(sessionId, shareId, options) ?? 0; + }, + setRoomShareControl( + sessionId: string, + shareId: string, + controllerUserId: string | null, + ): void { + provider()?.setRoomShareControl(sessionId, shareId, controllerUserId); + }, + /** How long the terminal keeps a detached session, or null while it is off. */ + idleTimeoutMinutes(): number | null { + return provider()?.idleTimeoutMinutes() ?? null; + }, +}; diff --git a/src/backend/hosts/session-sharing/live-sessions.ts b/src/backend/hosts/session-sharing/live-sessions.ts index 5ce82d297..d61a98019 100644 --- a/src/backend/hosts/session-sharing/live-sessions.ts +++ b/src/backend/hosts/session-sharing/live-sessions.ts @@ -1,4 +1,4 @@ -import { sessionManager } from "../terminal/session-manager.js"; +import { liveTerminalSessions } from "../live-terminal-sessions.js"; // Remote Desktop is a first-party plugin (plugins/remote-desktop); this goes // through the same in-core bridge its router is mounted behind, since // plugins/ is compiled separately from src/backend/ and core cannot @@ -43,7 +43,7 @@ export function isLiveSessionOwnedBy( userId: string, ): boolean { if (protocol === "ssh") { - const session = sessionManager.getSession(sessionId); + const session = liveTerminalSessions.getSession(sessionId); return !!session && session.isConnected && session.userId === userId; } const info = getGuacSessionInfo(sessionId); @@ -55,7 +55,7 @@ export function isLiveSession( sessionId: string, ): boolean { if (protocol === "ssh") { - const session = sessionManager.getSession(sessionId); + const session = liveTerminalSessions.getSession(sessionId); return !!session && session.isConnected; } return !!getGuacSessionInfo(sessionId); diff --git a/src/backend/hosts/session-sharing/routes.ts b/src/backend/hosts/session-sharing/routes.ts index b7585c860..028e9dd90 100644 --- a/src/backend/hosts/session-sharing/routes.ts +++ b/src/backend/hosts/session-sharing/routes.ts @@ -4,7 +4,7 @@ import type { AuthenticatedRequest } from "../../../types/index.js"; import { AuthManager } from "../../utils/auth-manager.js"; import { PermissionManager } from "../../utils/permission-manager.js"; import { sshLogger } from "../../utils/logger.js"; -import { sessionManager } from "../terminal/session-manager.js"; +import { liveTerminalSessions } from "../live-terminal-sessions.js"; import { isLiveSession, isLiveSessionOwnedBy, @@ -326,7 +326,7 @@ router.delete( // guac link only blocks *future* resolves until the guest's own socket ends. if (share.protocol === "ssh") { try { - sessionManager.ownerEndSession( + liveTerminalSessions.ownerEndSession( share.sessionId, "Session share revoked by owner", ); @@ -482,7 +482,7 @@ router.post( } if (share.protocol === "ssh") { - sessionManager.ownerEndSession( + liveTerminalSessions.ownerEndSession( share.sessionId, "Session ended by owner", ); diff --git a/src/backend/hosts/terminal-auth-helpers.ts b/src/backend/hosts/terminal-auth-helpers.ts index 6197aa61e..c14e71c39 100644 --- a/src/backend/hosts/terminal-auth-helpers.ts +++ b/src/backend/hosts/terminal-auth-helpers.ts @@ -12,7 +12,7 @@ import ssh2Pkg, { type KnownPublicKey = KnownPublicKeys[number]; -const { AgentProtocol, BaseAgent } = ssh2Pkg; +const { BaseAgent } = ssh2Pkg; const DEFAULT_PORT_KNOCK_TIMEOUT_MS = 1000; type Sleep = (ms: number) => Promise<void>; @@ -27,58 +27,6 @@ type PortKnockingOptions = { const sleep: Sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); -export class MemoryAgent extends BaseAgent { - private key: ParsedKey; - - constructor(key: ParsedKey) { - super(); - this.key = key; - } - - getIdentities(cb: IdentityCallback<ParsedKey>): void { - cb(null, [this.key]); - } - - getStream(cb: GetStreamCallback): void { - const protocol = new AgentProtocol(false); - - protocol.on("identities", (request) => { - protocol.getIdentitiesReply(request, [this.key]); - }); - - protocol.on("sign", (request, publicKey, data, options) => { - this.sign(publicKey, data, options, (error, signature) => { - if (error || !signature) return protocol.failureReply(request); - protocol.signReply(request, signature); - }); - }); - - cb(null, protocol); - } - - sign( - _pubKey: ParsedKey | Buffer | string, - data: Buffer, - optionsOrCb: SigningRequestOptions | SignCallback, - cb?: SignCallback, - ): void { - const callback = typeof optionsOrCb === "function" ? optionsOrCb : cb!; - const options = typeof optionsOrCb === "function" ? {} : optionsOrCb; - try { - const algo = - options.hash === "sha256" - ? "rsa-sha2-256" - : options.hash === "sha512" - ? "rsa-sha2-512" - : undefined; - const signature = this.key.sign(data, algo); - callback(null, signature); - } catch (err) { - callback(err instanceof Error ? err : new Error(String(err))); - } - } -} - export async function resolveAgentSocket( terminalConfig: Record<string, unknown> | undefined, ): Promise<{ socketPath: string } | { error: string }> { diff --git a/src/backend/hosts/terminal/index.ts b/src/backend/hosts/terminal/index.ts deleted file mode 100644 index 73096b3b3..000000000 --- a/src/backend/hosts/terminal/index.ts +++ /dev/null @@ -1,3097 +0,0 @@ -import { getErrorMessage } from "../../utils/error-message.js"; -import { getAuditUsername } from "../../utils/audit-logger.js"; -import { collabRoomHub } from "../collab/room-hub.js"; -import type { SessionShareRecord } from "../../database/repositories/session-share-repository.js"; -import { createCurrentCollabRoomRepository } from "../../database/repositories/factory.js"; -import { - parseWsMessage, - asObject, - asString, - toTerminalDimension, -} from "../../utils/ws-message.js"; -import { StringDecoder } from "string_decoder"; -import { WebSocketServer, WebSocket, type RawData } from "ws"; -import ssh2Pkg, { - type Client as SSHClientType, - type ClientChannel, - type PseudoTtyOptions, -} from "ssh2"; -const { Client, utils: ssh2Utils } = ssh2Pkg; -import axios from "axios"; -import { createCurrentHostResolutionRepository } from "../../database/repositories/factory.js"; -import { sshLogger, authLogger } from "../../utils/logger.js"; -import { logAudit } from "../../utils/audit-logger.js"; -import { AuthManager } from "../../utils/auth-manager.js"; -import { DataCrypto } from "../../utils/data-crypto.js"; -import { SSHAuthManager } from "../connect/terminal-prompt.js"; -import { buildConnectConfig } from "../connect/build-connect-config.js"; -import { - getSshAuthProvider, - listSshAuthProviders, -} from "../connect/auth-provider-registry.js"; -import { ensureCoreSshAuthProviders } from "../connect/core-providers.js"; -import { getHostSocks5Config, openSshTransport } from "../connect/transport.js"; -import type { - SshAuthEnv, - SshAuthProvider, - SshConnectHost, -} from "../connect/types.js"; -import { JumpHostChainError } from "../jump-host-chain.js"; -import { - sessionManager, - isMessageAllowedForParticipant, -} from "./session-manager.js"; -import { - createCurrentSessionShareRepository, - createCurrentSettingsRepository, -} from "../../database/repositories/factory.js"; -import { - detectTmux, - attachOrCreateTmuxSession, - waitForTmuxSession, -} from "../tmux/helper.js"; -import { MemoryAgent } from "../terminal-auth-helpers.js"; -import { isWindowsSftpPath, sftpPathToLocalPath } from "../transfer-paths.js"; -import { dispatchLoginEvent } from "../../utils/login-event-dispatch.js"; -import { getClientIp } from "../../utils/request-origin.js"; -import { isRetriableDnsError, resolveHostForSshConnect } from "../ssh-dns.js"; -import { - hostAddressMismatch, - HOST_ADDRESS_MISMATCH_MESSAGE, - HOST_NOT_ON_THIS_SERVER_MESSAGE, - resolveServerHostId, - resolveServerJumpHosts, -} from "../host-identity.js"; -import { extractWebSocketToken } from "../../utils/ws-auth.js"; -import { hostSessionStatus } from "../host-session-status.js"; - -class SshInteractionUnavailableError extends Error {} - -interface ConnectToHostData { - cols: number; - rows: number; - hostConfig: { - id: number; - /** Names the host across a sync pair; `id` only names it locally. */ - syncId?: string | null; - instanceId?: string; - ip: string; - port: number; - username: string; - password?: string; - key?: string; - keyPassword?: string; - keyType?: string; - authType?: string; - credentialId?: number; - userId?: string; - forceKeyboardInteractive?: boolean; - useWarpgate?: boolean; - jumpHosts?: Array<{ hostId: number }>; - useSocks5?: boolean; - socks5Host?: string; - socks5Port?: number; - socks5Username?: string; - socks5Password?: string; - socks5ProxyChain?: unknown; - portKnockSequence?: Array<{ - port: number; - protocol?: "tcp" | "udp"; - delay?: number; - }>; - terminalConfig?: { - keepaliveInterval?: number; - keepaliveCountMax?: number; - [key: string]: unknown; - }; - enableSessionLogging?: boolean; - /** When true, ignore key material and force password auth (fallback path). */ - passwordFallbackOnly?: boolean; - }; - initialPath?: string; - executeCommand?: string; - /** Attach straight to this tmux session once the shell is ready - * (tmux monitor opens its panes through a real PTY this way). */ - tmuxAttachSession?: string; -} - -interface ResizeData { - cols: number; - rows: number; -} - -interface TOTPResponseData { - code?: string; -} - -const authManager = AuthManager.getInstance(); - -const userConnections = new Map<string, Set<WebSocket>>(); - -// Re-exported so the ssh-terminal plugin can publish it on the service -// registry. session-manager itself stays in core: six modules outside the -// terminal import it directly. -export { sessionManager }; - -/** - * Serves /plugin-ws/ssh-terminal/terminal through the main backend server. - * - * `noServer` because core owns the listening socket now: the ssh-terminal - * plugin registers handleTerminalUpgrade below through ctx.ws and there is no - * terminal port any more. Handlers stay at module scope, so none of the - * connection logic below had to change. - */ -const wss = new WebSocketServer({ noServer: true }); - -wss.on("error", (error) => { - sshLogger.error("WebSocket server error", error, { - operation: "wss_error", - }); -}); - -/** - * Hands one upgrade to the terminal's own WebSocketServer. - * - * The connection handler below authenticates each socket itself, because a - * share-link guest arrives with a share token rather than a session. So this - * is registered as a public ctx.ws route and the auth stays where it already - * is, rather than being split across two places. - */ -export function handleTerminalUpgrade( - req: import("http").IncomingMessage, - socket: import("stream").Duplex, - head: Buffer, -): void { - wss.handleUpgrade(req, socket, head, (ws) => { - wss.emit("connection", ws, req); - }); -} - -export async function stopTerminalServer(): Promise<void> { - // No port to free any more: the route is unmounted by the runtime. The live - // sockets still have to be closed, because a terminal session would - // otherwise outlive the plugin that owns it. - for (const ws of wss.clients) { - try { - ws.close(1001, "Terminal plugin disabled"); - } catch { - // Already gone. - } - } - - sshLogger.info("Terminal WebSocket sessions closed", { - operation: "terminal_ws_stop", - }); -} - -/** - * Auth path for anonymous share-link guests (?shareToken=<linkToken>). - * Never touches DataCrypto/user credentials - guests join an already-live - * stream and never decrypt stored secrets. - */ -async function handleShareTokenConnection( - ws: WebSocket, - req: import("http").IncomingMessage, - shareToken: string, -): Promise<void> { - const share = - await createCurrentSessionShareRepository().findByLinkToken(shareToken); - if (!share) { - ws.close(1008, "Invalid or expired share link"); - return; - } - return attachShareGuest(ws, req, share); -} - -/** - * Auth path for anonymous collab-room guests (?roomGuestToken=<token>): the - * room's guest link resolves to whatever share is on stage right now. - */ -async function handleRoomGuestConnection( - ws: WebSocket, - req: import("http").IncomingMessage, - roomGuestToken: string, -): Promise<void> { - const { isCollabGuestRateLimited } = - await import("../collab/guest-rate-limit.js"); - const ip = req.socket.remoteAddress ?? "unknown"; - if (isCollabGuestRateLimited(ip)) { - ws.close(1008, "Too many requests"); - return; - } - const room = - await createCurrentCollabRoomRepository().findByGuestToken(roomGuestToken); - const share = room?.stageShareId - ? await createCurrentSessionShareRepository().findActiveById( - room.stageShareId, - ) - : null; - if (!share) { - ws.close(1008, "Nothing is being presented"); - return; - } - return attachShareGuest(ws, req, share); -} - -/** Joins an anonymous guest socket to a live shared SSH session, read-only or not per the share. */ -async function attachShareGuest( - ws: WebSocket, - req: import("http").IncomingMessage, - share: SessionShareRecord, -): Promise<void> { - const shareRepo = createCurrentSessionShareRepository(); - if (share.protocol !== "ssh") { - ws.close(1008, "Unsupported share protocol"); - return; - } - - const globallyEnabled = await createCurrentSettingsRepository().getBoolean( - "session_sharing_globally_enabled", - true, - ); - if (!globallyEnabled) { - ws.close(1008, "Session sharing is disabled"); - return; - } - - const host = await createCurrentHostResolutionRepository().findHostById( - share.hostId, - share.ownerUserId, - ); - if (!host || host.allowSessionSharing === false) { - ws.close(1008, "Session sharing is disabled for this host"); - return; - } - - const session = sessionManager.getSession(share.sessionId); - if (!session || !session.isConnected) { - ws.close(1008, "Session has ended"); - return; - } - - const permissionLevel = share.permissionLevel as "read-write" | "read-only"; - const joined = sessionManager.joinAsParticipant(share.sessionId, ws, { - userId: null, - permissionLevel, - guestLabel: "Guest", - shareId: share.id, - }); - if (!joined) { - ws.close(1008, "Session is no longer active"); - return; - } - - shareRepo.touchShareUsage(share.id).catch(() => {}); - shareRepo.recordParticipantJoin(share.id, null, "Guest").catch(() => {}); - - const buffered = sessionManager.getBuffer(joined); - if (buffered) { - ws.send(JSON.stringify({ type: "data", data: buffered })); - } - ws.send( - JSON.stringify({ type: "sessionAttached", sessionId: share.sessionId }), - ); - ws.send(JSON.stringify({ type: "connected", message: "Joined session" })); - - const currentSessionId: string = share.sessionId; - - let wsAlive = true; - ws.on("pong", () => { - wsAlive = true; - }); - const wsPingInterval = setInterval(() => { - if (ws.readyState === WebSocket.OPEN) { - if (!wsAlive) { - ws.terminate(); - return; - } - wsAlive = false; - ws.ping(); - } else { - clearInterval(wsPingInterval); - } - }, 30000); - - ws.on("close", () => { - clearInterval(wsPingInterval); - sessionManager.removeParticipant(currentSessionId, ws); - sshLogger.info("Guest left shared terminal session", { - operation: "terminal_guest_disconnect", - sessionId: currentSessionId, - shareId: share.id, - }); - }); - - ws.on("message", (msg: RawData) => { - let type: string; - let data: unknown; - try { - ({ type, data } = parseWsMessage(msg)); - } catch { - return; - } - - const liveSession = sessionManager.getSession(currentSessionId); - const participant = liveSession - ? sessionManager.getParticipantForWs(liveSession, ws) - : null; - if (!isMessageAllowedForParticipant(participant, type)) { - return; - } - - switch (type) { - case "input": { - if (typeof data !== "string") break; - const inputData = data; - sessionManager.bufferInput(currentSessionId, inputData); - const inputStream = liveSession?.sshStream; - if (inputStream) { - try { - inputStream.write(Buffer.from(inputData, "utf8")); - } catch { - inputStream.write(Buffer.from(inputData, "latin1")); - } - } - break; - } - case "ping": - ws.send(JSON.stringify({ type: "pong" })); - break; - case "disconnect": - sessionManager.removeParticipant(currentSessionId, ws); - break; - default: - break; - } - }); -} - -wss.on("connection", async (ws: WebSocket, req) => { - let userId: string | undefined; - let sessionId: string | undefined; - - ws.on("error", (error) => { - sshLogger.error("WebSocket connection error", error, { - operation: "ws_error", - sessionId, - }); - }); - - const urlObj = new URL(req.url || "", "http://localhost"); - const shareToken = urlObj.searchParams.get("shareToken"); - - if (shareToken) { - await handleShareTokenConnection(ws, req, shareToken); - return; - } - const roomGuestToken = urlObj.searchParams.get("roomGuestToken"); - if (roomGuestToken) { - await handleRoomGuestConnection(ws, req, roomGuestToken); - return; - } - - try { - const token = extractWebSocketToken(req); - - if (!token) { - ws.close(1008, "Authentication required"); - return; - } - - const payload = await authManager.verifyJWTToken(token); - if (!payload?.userId || payload.pendingTOTP) { - ws.close(1008, "Authentication required"); - return; - } - - userId = payload.userId; - sessionId = payload.sessionId; - } catch (error) { - sshLogger.error( - "WebSocket JWT verification failed during connection", - error, - { - operation: "websocket_connection_auth_error", - ip: getClientIp(req), - }, - ); - ws.close(1008, "Authentication required"); - return; - } - - const dataKey = DataCrypto.getUserDataKey(userId); - if (!dataKey) { - ws.send( - JSON.stringify({ - type: "error", - message: "Data locked - re-authenticate with password", - code: "DATA_LOCKED", - }), - ); - ws.close(1008, "Data access required"); - return; - } - - if (!userConnections.has(userId)) { - userConnections.set(userId, new Set()); - } - const userWs = userConnections.get(userId)!; - userWs.add(ws); - sshLogger.info("Terminal WebSocket connection established", { - operation: "terminal_ws_connect", - sessionId, - userId, - }); - - let currentSessionId: string | null = null; - let sshConn: SSHClientType | null = null; - let sshStream: ClientChannel | null = null; - let lastJumpClient: SSHClientType | null = null; - let keyboardInteractiveFinish: ((responses: string[]) => void) | null = null; - let totpPromptSent = false; - let totpTimeout: NodeJS.Timeout | null = null; - let isKeyboardInteractive = false; - let keyboardInteractiveResponded = false; - let isConnecting = false; - let isConnected = false; - let isCleaningUp = false; - let isShellInitializing = false; - let isDuplicateConnDiscarded = false; - let warpgateAuthPromptSent = false; - let warpgateAuthTimeout: NodeJS.Timeout | null = null; - let isAwaitingAuthCredentials = false; - - let wsAlive = true; - - ws.on("pong", () => { - wsAlive = true; - }); - - const wsPingInterval = setInterval(() => { - if (ws.readyState === WebSocket.OPEN) { - if (!wsAlive) { - sshLogger.warn( - "WebSocket pong timeout - terminating zombie connection", - { - operation: "ws_pong_timeout", - userId, - sessionId: currentSessionId, - }, - ); - ws.terminate(); - return; - } - wsAlive = false; - ws.ping(); - } - }, 30000); - - ws.on("close", () => { - clearInterval(wsPingInterval); - collabRoomHub.unsubscribe(ws); - sshLogger.info("Terminal WebSocket disconnected", { - operation: "terminal_ws_disconnect", - sessionId, - userId, - }); - const userWs = userConnections.get(userId); - if (userWs) { - userWs.delete(ws); - if (userWs.size === 0) { - userConnections.delete(userId); - } - } - - if (currentSessionId) { - const session = sessionManager.getSession(currentSessionId); - if (session?.isConnected) { - const participant = sessionManager.getParticipantForWs(session, ws); - if (participant && !participant.isOwner) { - sessionManager.removeParticipant(currentSessionId, ws); - } else { - // Only detach if this WS is still the owner's attached socket, or - // no owner is currently attached. If a refresh reconnected and - // reattached a new WS before this close event fired, we must not - // clobber that new attachment. - const ownerStillAttached = Array.from( - session.participants.values(), - ).some((p) => p.isOwner && p.ws !== ws); - if (!ownerStillAttached) { - sessionManager.detachWs(currentSessionId); - } - } - } else { - sessionManager.destroySession(currentSessionId); - currentSessionId = null; - } - } - cleanupAuthState(); - }); - - function resetConnectionState() { - isConnecting = false; - isConnected = false; - isKeyboardInteractive = false; - keyboardInteractiveResponded = false; - keyboardInteractiveFinish = null; - totpPromptSent = false; - warpgateAuthPromptSent = false; - } - - ws.on("message", async (msg: RawData) => { - const currentDataKey = DataCrypto.getUserDataKey(userId); - if (!currentDataKey) { - ws.send( - JSON.stringify({ - type: "error", - message: "Data access expired - please re-authenticate", - code: "DATA_EXPIRED", - }), - ); - ws.close(1008, "Data access expired"); - return; - } - - let type: string; - let data: unknown; - try { - ({ type, data } = parseWsMessage(msg)); - } catch (e) { - sshLogger.warn("Rejected malformed WebSocket message", { - operation: "websocket_message_invalid", - userId, - error: getErrorMessage(e), - }); - ws.send(JSON.stringify({ type: "error", message: "Invalid message" })); - return; - } - - // Server-side gate: non-owner participants (read-only or read-write - // guests/joiners) may only send input/ping/disconnect - everything else - // (auth flows, tmux, resize, etc.) is owner-only and silently ignored. - if (type !== "joinSharedSession") { - const gateSession = currentSessionId - ? sessionManager.getSession(currentSessionId) - : null; - const gateParticipant = gateSession - ? sessionManager.getParticipantForWs(gateSession, ws) - : null; - if (!isMessageAllowedForParticipant(gateParticipant, type)) { - return; - } - } - - try { - switch (type) { - case "connectToHost": { - const connectData = data as ConnectToHostData; - if (!connectData?.hostConfig) { - ws.send( - JSON.stringify({ - type: "error", - message: "Missing host configuration", - }), - ); - break; - } - connectData.hostConfig.userId = userId; - handleConnectToHost(connectData).catch((error) => { - const errMsg = getErrorMessage(error); - if ( - errMsg.includes("Cannot parse privateKey") && - errMsg.includes("no passphrase") - ) { - isAwaitingAuthCredentials = true; - ws.send( - JSON.stringify({ - type: "passphrase_required", - message: - "The SSH key is encrypted. Please enter the passphrase to unlock it.", - }), - ); - return; - } - sshLogger.error("Failed to connect to host", error, { - operation: "ssh_connect", - userId, - hostId: connectData.hostConfig?.id, - ip: connectData.hostConfig?.ip, - }); - ws.send( - JSON.stringify({ - type: "error", - message: "Failed to connect to host: " + errMsg, - }), - ); - }); - break; - } - - case "attachSession": { - const attachData = data as { - sessionId: string; - cols: number; - rows: number; - tabInstanceId?: string; - }; - sshLogger.info("Attempting to attach session", { - operation: "terminal_attach_session", - sessionId: attachData.sessionId, - tabInstanceId: attachData.tabInstanceId, - userId, - requestedCols: attachData.cols, - requestedRows: attachData.rows, - }); - const session = sessionManager.attachWs( - attachData.sessionId, - userId, - ws, - attachData.tabInstanceId, - ); - if (session) { - sshLogger.success("Session attached successfully", { - operation: "terminal_attach_success", - sessionId: attachData.sessionId, - sessionCreatedAt: session.createdAt, - wasDetached: !!session.lastDetachedAt, - detachedDuration: session.lastDetachedAt - ? Date.now() - session.lastDetachedAt - : 0, - }); - currentSessionId = attachData.sessionId; - sshStream = session.sshStream; - sshConn = session.sshConn; - isConnecting = false; - isConnected = true; - const buffered = sessionManager.getBuffer(session); - if (buffered) { - ws.send(JSON.stringify({ type: "data", data: buffered })); - } - const attachCols = toTerminalDimension(attachData.cols); - const attachRows = toTerminalDimension(attachData.rows); - if ( - attachCols && - attachRows && - (attachCols !== session.cols || attachRows !== session.rows) - ) { - session.sshStream?.setWindow( - attachRows, - attachCols, - attachRows, - attachCols, - ); - session.cols = attachCols; - session.rows = attachRows; - } - - ws.send( - JSON.stringify({ - type: "sessionAttached", - sessionId: attachData.sessionId, - }), - ); - ws.send( - JSON.stringify({ - type: "connected", - message: "Session reattached", - }), - ); - } else { - sshLogger.warn( - "Session attachment failed - will create new connection", - { - operation: "terminal_attach_failed", - sessionId: attachData.sessionId, - tabInstanceId: attachData.tabInstanceId, - userId, - reason: "session_not_found_or_invalid", - }, - ); - ws.send( - JSON.stringify({ - type: "sessionExpired", - sessionId: attachData.sessionId, - }), - ); - } - break; - } - - case "listSessions": { - const sessions = sessionManager.getUserSessions(userId); - ws.send( - JSON.stringify({ - type: "sessionList", - sessions: sessions.map((s) => ({ - id: s.id, - hostId: s.hostId, - hostName: s.hostName, - createdAt: s.createdAt, - lastDetachedAt: s.lastDetachedAt, - tmuxSessionName: s.tmuxSessionName, - })), - }), - ); - break; - } - - case "resize": { - const resizeData = data as ResizeData; - handleResize(resizeData); - break; - } - - case "disconnect": { - const disconnectSession = currentSessionId - ? sessionManager.getSession(currentSessionId) - : null; - const disconnectParticipant = disconnectSession - ? sessionManager.getParticipantForWs(disconnectSession, ws) - : null; - if (disconnectParticipant && !disconnectParticipant.isOwner) { - if (currentSessionId) { - sessionManager.removeParticipant(currentSessionId, ws); - currentSessionId = null; - } - break; - } - if (currentSessionId) { - sessionManager.destroySession(currentSessionId); - currentSessionId = null; - } - cleanupAuthState(); - sshConn = null; - sshStream = null; - break; - } - - case "get_cwd": { - const activeConn = - sessionManager.getSession(currentSessionId)?.sshConn ?? sshConn; - if (!activeConn) { - ws.send(JSON.stringify({ type: "cwd", path: "/" })); - break; - } - activeConn.exec("pwd", (err, execStream) => { - if (err) { - ws.send(JSON.stringify({ type: "cwd", path: "/" })); - return; - } - let stdout = ""; - execStream.on("data", (chunk: Buffer) => { - stdout += chunk.toString("utf-8"); - }); - execStream.stderr.on("data", () => {}); - execStream.on("close", () => { - const cwd = stdout.trim() || "/"; - if (ws.readyState === WebSocket.OPEN) { - ws.send(JSON.stringify({ type: "cwd", path: cwd })); - } - }); - }); - break; - } - - case "open_file_in_editor": { - const requestedPath = asString(asObject(data).path); - const activeConn = - sessionManager.getSession(currentSessionId)?.sshConn ?? sshConn; - if (!activeConn || !requestedPath) { - ws.send( - JSON.stringify({ - type: "open_file_in_editor", - path: requestedPath || "/", - }), - ); - break; - } - const escapedPath = requestedPath.replace(/'/g, "'\\''"); - activeConn.exec( - `realpath '${escapedPath}' 2>/dev/null || echo '${escapedPath}'`, - (err, execStream) => { - if (err) { - ws.send( - JSON.stringify({ - type: "open_file_in_editor", - path: requestedPath, - }), - ); - return; - } - let stdout = ""; - execStream.on("data", (chunk: Buffer) => { - stdout += chunk.toString("utf-8"); - }); - execStream.stderr.on("data", () => {}); - execStream.on("close", () => { - const resolvedPath = stdout.trim() || requestedPath; - if (ws.readyState === WebSocket.OPEN) { - ws.send( - JSON.stringify({ - type: "open_file_in_editor", - path: resolvedPath, - }), - ); - } - }); - }, - ); - break; - } - - case "input": { - if (typeof data !== "string") break; - const inputData = data; - if (currentSessionId) { - sessionManager.bufferInput(currentSessionId, inputData); - } - const inputStream = - sessionManager.getSession(currentSessionId)?.sshStream ?? sshStream; - if (inputStream) { - if (inputData === "\t") { - inputStream.write(inputData); - } else if ( - typeof inputData === "string" && - inputData.startsWith("\x1b") - ) { - inputStream.write(inputData); - } else { - try { - inputStream.write(Buffer.from(inputData, "utf8")); - } catch (error) { - sshLogger.error("Error writing input to SSH stream", error, { - operation: "ssh_input_encoding", - userId, - dataLength: inputData.length, - }); - inputStream.write(Buffer.from(inputData, "latin1")); - } - } - } - break; - } - - case "ping": - ws.send(JSON.stringify({ type: "pong" })); - break; - - case "tmux_attach": { - const tmuxData = data as { sessionName: string }; - const session = currentSessionId - ? sessionManager.getSession(currentSessionId) - : null; - if (session?.sshStream) { - const existingName = tmuxData.sessionName || undefined; - if (existingName) { - attachOrCreateTmuxSession(session.sshStream, existingName); - session.tmuxSessionName = existingName; - sshLogger.info("User selected tmux session to attach", { - operation: "tmux_user_attach", - sessionName: existingName, - hostId: session.hostId, - }); - ws.send( - JSON.stringify({ - type: "tmux_session_attached", - sessionName: existingName, - }), - ); - } else { - const newName = `termix-${session.hostId}-${Date.now().toString(36).slice(-4)}`; - attachOrCreateTmuxSession(session.sshStream, undefined, newName); - const sshConn = session.sshConn; - if (sshConn) { - (async () => { - const confirmed = await waitForTmuxSession(sshConn, newName); - session.tmuxSessionName = confirmed; - sshLogger.info("User requested new tmux session", { - operation: "tmux_user_create", - sessionName: confirmed, - hostId: session.hostId, - }); - ws.send( - JSON.stringify({ - type: "tmux_session_created", - sessionName: confirmed, - }), - ); - })(); - } - } - } - break; - } - - case "tmux_detach": { - const session = currentSessionId - ? sessionManager.getSession(currentSessionId) - : null; - if (session?.sshConn && session.tmuxSessionName) { - const tmuxName = session.tmuxSessionName; - session.sshStream?.write("\x02d"); - session.tmuxSessionName = null; - sshLogger.info("User detached from tmux session", { - operation: "tmux_user_detach", - sessionName: tmuxName, - hostId: session.hostId, - }); - ws.send( - JSON.stringify({ type: "tmux_detached", sessionName: tmuxName }), - ); - } - break; - } - - case "totp_response": { - const totpData = data as TOTPResponseData; - if (keyboardInteractiveFinish && totpData?.code) { - if (totpTimeout) { - clearTimeout(totpTimeout); - totpTimeout = null; - } - const totpCode = totpData.code; - keyboardInteractiveFinish([totpCode]); - keyboardInteractiveFinish = null; - totpPromptSent = false; - } else { - sshLogger.warn("TOTP response received but no callback available", { - operation: "totp_response_error", - userId, - hasCallback: !!keyboardInteractiveFinish, - hasCode: !!totpData?.code, - }); - ws.send( - JSON.stringify({ - type: "error", - message: "TOTP authentication state lost. Please reconnect.", - }), - ); - } - break; - } - - case "password_response": { - const passwordData = data as TOTPResponseData; - if (keyboardInteractiveFinish && passwordData?.code) { - if (totpTimeout) { - clearTimeout(totpTimeout); - totpTimeout = null; - } - const password = passwordData.code; - keyboardInteractiveFinish([password]); - keyboardInteractiveFinish = null; - } else { - sshLogger.warn( - "Password response received but no callback available", - { - operation: "password_response_error", - userId, - hasCallback: !!keyboardInteractiveFinish, - hasCode: !!passwordData?.code, - }, - ); - ws.send( - JSON.stringify({ - type: "error", - message: - "Password authentication state lost. Please reconnect.", - }), - ); - } - break; - } - - case "warpgate_auth_continue": { - if (keyboardInteractiveFinish) { - if (warpgateAuthTimeout) { - clearTimeout(warpgateAuthTimeout); - warpgateAuthTimeout = null; - } - keyboardInteractiveFinish([""]); - keyboardInteractiveFinish = null; - warpgateAuthPromptSent = false; - } - break; - } - - case "reconnect_with_credentials": { - const credentialsData = data as { - cols: number; - rows: number; - hostConfig: ConnectToHostData["hostConfig"]; - password?: string; - sshKey?: string; - keyPassword?: string; - }; - - if (!credentialsData?.hostConfig) { - ws.send( - JSON.stringify({ - type: "error", - message: "Missing host configuration", - }), - ); - break; - } - - if (credentialsData.password) { - credentialsData.hostConfig.password = credentialsData.password; - credentialsData.hostConfig.authType = "password"; - ( - credentialsData.hostConfig as Record<string, unknown> - ).userProvidedPassword = true; - } else if (credentialsData.sshKey) { - credentialsData.hostConfig.key = credentialsData.sshKey; - credentialsData.hostConfig.keyPassword = - credentialsData.keyPassword; - credentialsData.hostConfig.authType = "key"; - } else if (credentialsData.keyPassword) { - credentialsData.hostConfig.keyPassword = - credentialsData.keyPassword; - } - - isAwaitingAuthCredentials = false; - if (currentSessionId) { - sessionManager.destroySession(currentSessionId); - currentSessionId = null; - } - cleanupAuthState(); - sshConn = null; - sshStream = null; - - const reconnectData: ConnectToHostData = { - cols: credentialsData.cols, - rows: credentialsData.rows, - hostConfig: credentialsData.hostConfig, - }; - - handleConnectToHost(reconnectData).catch((error) => { - const errMsg = getErrorMessage(error); - if ( - errMsg.includes("Cannot parse privateKey") && - errMsg.includes("no passphrase") - ) { - isAwaitingAuthCredentials = true; - ws.send( - JSON.stringify({ - type: "passphrase_required", - message: - "The SSH key is encrypted. Please enter the passphrase to unlock it.", - }), - ); - return; - } - sshLogger.error("Failed to reconnect with credentials", error, { - operation: "ssh_reconnect_with_credentials", - userId, - hostId: credentialsData.hostConfig?.id, - ip: credentialsData.hostConfig?.ip, - }); - ws.send( - JSON.stringify({ - type: "error", - message: - "Failed to connect with provided credentials: " + errMsg, - }), - ); - }); - break; - } - - case "opkssh_start_auth": { - const opksshData = data as { hostId: number }; - try { - await startAuthInteraction("opkssh", opksshData.hostId, data); - } catch (error) { - sshLogger.error("Failed to start OPKSSH auth", error, { - operation: "opkssh_start_auth_error", - userId, - hostId: opksshData.hostId, - }); - ws.send( - JSON.stringify({ - type: "opkssh_error", - requestId: "", - error: - error instanceof SshInteractionUnavailableError - ? error.message - : "Failed to start OPKSSH authentication", - }), - ); - } - break; - } - - case "opkssh_cancel": { - { - const { cancelStepCaAuth } = await import("../step-ca-auth.js"); - cancelStepCaAuth( - String((data as { requestId?: string })?.requestId ?? ""), - ); - } - const cancelData = data as { requestId: string }; - try { - const { cancelAuthSession } = await import("../opkssh-auth.js"); - cancelAuthSession(cancelData.requestId); - resetConnectionState(); - } catch (error) { - sshLogger.error("Failed to cancel OPKSSH auth", error, { - operation: "opkssh_cancel_error", - userId, - }); - } - break; - } - - case "opkssh_browser_opened": { - break; - } - - case "opkssh_auth_completed": { - const completedData = data as { - hostId: number; - cols?: number; - rows?: number; - hostConfig?: ConnectToHostData["hostConfig"]; - }; - - resetConnectionState(); - - const reconnectConfig: ConnectToHostData = { - cols: completedData.cols || 80, - rows: completedData.rows || 24, - hostConfig: - completedData.hostConfig || - ({ - id: completedData.hostId, - ip: "", - port: 22, - username: "", - userId, - } as ConnectToHostData["hostConfig"]), - }; - - handleConnectToHost(reconnectConfig).catch((error) => { - sshLogger.error("Failed to reconnect after OPKSSH auth", error, { - operation: "opkssh_reconnect_error", - userId, - hostId: completedData.hostId, - }); - ws.send( - JSON.stringify({ - type: "error", - message: - "Failed to connect after authentication: " + - getErrorMessage(error), - }), - ); - }); - break; - } - - case "vault_start_auth": { - const vaultData = data as { hostId: number }; - try { - await startAuthInteraction("vault", vaultData.hostId, data); - } catch (error) { - sshLogger.error("Failed to start Vault auth", error, { - operation: "vault_start_auth_error", - userId, - hostId: vaultData.hostId, - }); - ws.send( - JSON.stringify({ - type: "vault_error", - hostId: vaultData.hostId, - error: getErrorMessage( - error, - "Failed to start Vault authentication", - ), - }), - ); - } - break; - } - - case "vault_cancel": { - const cancelData = data as { hostId: number }; - try { - const { cancelVaultAuthByHost } = - await import("../vault-oidc-auth.js"); - cancelVaultAuthByHost(userId, cancelData.hostId); - resetConnectionState(); - } catch (error) { - sshLogger.error("Failed to cancel Vault auth", error, { - operation: "vault_cancel_error", - userId, - }); - } - break; - } - - case "vault_auth_completed": { - const completedData = data as { - hostId: number; - cols?: number; - rows?: number; - hostConfig?: ConnectToHostData["hostConfig"]; - }; - - resetConnectionState(); - - const reconnectConfig: ConnectToHostData = { - cols: completedData.cols || 80, - rows: completedData.rows || 24, - hostConfig: - completedData.hostConfig || - ({ - id: completedData.hostId, - ip: "", - port: 22, - username: "", - userId, - } as ConnectToHostData["hostConfig"]), - }; - - handleConnectToHost(reconnectConfig).catch((error) => { - sshLogger.error("Failed to reconnect after Vault auth", error, { - operation: "vault_reconnect_error", - userId, - hostId: completedData.hostId, - }); - ws.send( - JSON.stringify({ - type: "error", - message: - "Failed to connect after authentication: " + - getErrorMessage(error), - }), - ); - }); - break; - } - - case "collab_subscribe": { - const { roomId } = (data ?? {}) as { roomId?: string }; - if (typeof roomId !== "string" || !roomId) break; - try { - const repository = createCurrentCollabRoomRepository(); - const room = await repository.findById(roomId); - const member = - room && !room.endedAt - ? await repository.findMember(roomId, userId) - : null; - if (!member) { - ws.send( - JSON.stringify({ - type: "error", - message: "Room not found", - }), - ); - break; - } - collabRoomHub.subscribe(roomId, { - ws, - userId, - username: await getAuditUsername(userId), - }); - } catch (error) { - sshLogger.error("Failed to subscribe to collab room", error, { - operation: "collab_subscribe_error", - userId, - }); - } - break; - } - - case "collab_unsubscribe": { - const { roomId } = (data ?? {}) as { roomId?: string }; - collabRoomHub.unsubscribe( - ws, - typeof roomId === "string" ? roomId : undefined, - ); - break; - } - - case "joinSharedSession": { - const joinData = data as { shareId: string; tabInstanceId?: string }; - try { - const shareRepo = createCurrentSessionShareRepository(); - const share = await shareRepo.findActiveById(joinData.shareId); - // Room-stage shares are joinable by any member of the live - // room whose stage points at them; user shares only by their - // target. - let eligible = - !!share && - share.protocol === "ssh" && - share.shareType === "user" && - share.targetUserId === userId; - if ( - !eligible && - share && - share.protocol === "ssh" && - share.shareType === "room" - ) { - const { canJoinRoomStageShare } = - await import("../collab/room-share-access.js"); - eligible = await canJoinRoomStageShare(share.id, userId); - } - if (!eligible || !share) { - ws.send( - JSON.stringify({ - type: "error", - message: "Share not found or not accessible", - }), - ); - break; - } - - // Room membership is the authorization for a room stage; the - // read-only share never exposes host credentials or config. - const { PermissionManager } = - await import("../../utils/permission-manager.js"); - const access = - share.shareType === "room" - ? { hasAccess: true } - : await PermissionManager.getInstance().canAccessHost( - userId, - share.hostId, - "connect", - ); - if (!access.hasAccess) { - ws.send( - JSON.stringify({ - type: "error", - message: "Share not found or not accessible", - }), - ); - break; - } - - const joinedSession = sessionManager.joinAsParticipant( - share.sessionId, - ws, - { - userId, - permissionLevel: share.permissionLevel as - "read-write" | "read-only", - displayName: await getAuditUsername(userId), - tabInstanceId: joinData.tabInstanceId, - shareId: share.id, - }, - ); - if (!joinedSession) { - ws.send( - JSON.stringify({ - type: "error", - message: "Shared session is no longer active", - }), - ); - break; - } - - currentSessionId = share.sessionId; - sshStream = joinedSession.sshStream; - sshConn = joinedSession.sshConn; - isConnecting = false; - isConnected = true; - - shareRepo.touchShareUsage(share.id).catch(() => {}); - shareRepo - .recordParticipantJoin(share.id, userId, null) - .catch(() => {}); - - const buffered = sessionManager.getBuffer(joinedSession); - if (buffered) { - ws.send(JSON.stringify({ type: "data", data: buffered })); - } - ws.send( - JSON.stringify({ - type: "sessionAttached", - sessionId: share.sessionId, - }), - ); - ws.send( - JSON.stringify({ type: "connected", message: "Joined session" }), - ); - } catch (error) { - sshLogger.error("Failed to join shared session", error, { - operation: "terminal_join_shared_session_error", - userId, - shareId: joinData.shareId, - }); - ws.send( - JSON.stringify({ - type: "error", - message: "Failed to join shared session", - }), - ); - } - break; - } - - default: - sshLogger.warn("Unknown message type received", { - operation: "websocket_message_unknown_type", - userId, - messageType: type, - }); - } - } catch (error) { - // A malformed payload must never escape into the process-level - // unhandledRejection handler, which exits the server. - sshLogger.error("Error handling WebSocket message", error, { - operation: "websocket_message_handler_error", - userId, - messageType: type, - }); - if (ws.readyState === WebSocket.OPEN) { - ws.send( - JSON.stringify({ - type: "error", - message: "Failed to process message", - }), - ); - } - } - }); - - /** - * Starts the browser sign-in behind an "<interaction>_auth_required" - * message. The provider for the host's auth type wins; otherwise the first - * provider that owns this interaction (Vault attaches to any auth type). - */ - async function startAuthInteraction( - interaction: string, - hostId: number, - payload: unknown, - ): Promise<void> { - ensureCoreSshAuthProviders(); - const host = await createCurrentHostResolutionRepository().findHostById( - hostId, - userId, - ); - if (!host) throw new SshInteractionUnavailableError("Host not found"); - - const own = getSshAuthProvider(host.authType as string); - const provider = - own?.interaction === interaction && own.startInteraction - ? own - : listSshAuthProviders().find( - (candidate) => - candidate.interaction === interaction && - candidate.startInteraction, - ); - if (!provider?.startInteraction) { - throw new SshInteractionUnavailableError( - `No enabled provider handles ${interaction} sign-in`, - ); - } - - const { getRequestOrigin } = await import("../../utils/request-origin.js"); - await provider.startInteraction({ - userId, - hostId, - host: { - name: host.name as string | null, - ip: host.ip as string, - username: host.username as string, - }, - socket: ws, - requestOrigin: getRequestOrigin(req), - payload: (payload ?? {}) as Record<string, unknown>, - }); - } - - async function handleConnectToHost(data: ConnectToHostData) { - const { hostConfig, initialPath, executeCommand, tmuxAttachSession } = data; - const { - id, - syncId: hostSyncId, - ip: rawIp, - port: clientPort, - username: clientUsername, - password, - key, - keyPassword, - keyType, - authType, - credentialId, - } = hostConfig; - const clientIp = rawIp?.replace(/^\[|\]$/g, "").trim() || rawIp; - let ip = clientIp; - let port = clientPort; - let username = clientUsername; - sshLogger.info("Resolving SSH host configuration", { - operation: "terminal_host_resolve", - sessionId, - userId, - hostId: id, - }); - - const sendLog = ( - stage: string, - level: string, - message: string, - details?: Record<string, unknown>, - ) => { - ws.send( - JSON.stringify({ - type: "connection_log", - data: { stage, level, message, details }, - }), - ); - }; - - if (!username || typeof username !== "string" || username.trim() === "") { - sshLogger.error("Invalid username provided", undefined, { - operation: "ssh_connect", - hostId: id, - ip, - }); - ws.send( - JSON.stringify({ type: "error", message: "Invalid username provided" }), - ); - return; - } - - if (!ip || typeof ip !== "string" || ip.trim() === "") { - sshLogger.error("Invalid IP provided", undefined, { - operation: "ssh_connect", - hostId: id, - username, - }); - ws.send( - JSON.stringify({ type: "error", message: "Invalid IP provided" }), - ); - return; - } - - if (!port || typeof port !== "number" || port <= 0) { - sshLogger.error("Invalid port provided", undefined, { - operation: "ssh_connect", - hostId: id, - ip, - username, - port, - }); - ws.send( - JSON.stringify({ type: "error", message: "Invalid port provided" }), - ); - return; - } - - if (isConnecting || isConnected) { - sshLogger.warn("Connection already in progress or established", { - operation: "ssh_connect", - hostId: id, - isConnecting, - isConnected, - }); - ws.send( - JSON.stringify({ - type: "error", - message: "Connection already in progress", - code: "DUPLICATE_CONNECTION", - }), - ); - return; - } - - isConnecting = true; - sshConn = new Client(); - - sendLog("dns", "info", `Starting address resolution of ${ip}`); - sendLog("tcp", "info", `Connecting to ${ip} port ${port}`); - - const onConnectionTimeout = () => { - if (sshConn && isConnecting && !isConnected) { - sshLogger.error("SSH connection timeout", undefined, { - operation: "ssh_connect", - hostId: id, - ip, - port, - username, - }); - ws.send( - JSON.stringify({ type: "error", message: "SSH connection timeout" }), - ); - if (currentSessionId) { - sessionManager.destroySession(currentSessionId); - currentSessionId = null; - } - cleanupAuthState(connectionTimeout); - } - }; - - // Reassigned when an auth provider's onBanner holds the connection open - // (Tailscale SSH check mode), so the short connect timeout does not tear - // down a connection the server is deliberately waiting on. - let connectionTimeout = setTimeout(onConnectionTimeout, 120000); - - let bannerHoldPending = false; - let authRetries = 0; - let isAuthRetrying = false; - let connectProvider: SshAuthProvider | null = null; - let connectEnv: SshAuthEnv | null = null; - let connectTarget: SshConnectHost | null = null; - let clearOnlineStatus: (() => void) | null = null; - - let resolvedHostData: - | (Record<string, unknown> & { - id?: number; - ip?: string; - port?: number; - username?: string; - password?: string; - key?: string; - keyPassword?: string; - keyType?: string; - authType?: string; - jumpHosts?: Array<{ hostId: number }>; - useSocks5?: boolean; - socks5Host?: string; - socks5Port?: number; - socks5Username?: string; - socks5Password?: string; - socks5ProxyChain?: unknown; - portKnockSequence?: ConnectToHostData["hostConfig"]["portKnockSequence"]; - terminalConfig?: ConnectToHostData["hostConfig"]["terminalConfig"]; - enableSessionLogging?: boolean; - }) - | null = null; - - if (id && userId) { - try { - const { resolveHostById, resolveHostBySyncId } = - await import("../host-resolver.js"); - - // Prefer the sync identity. A numeric id belongs to whichever database - // produced it, so on a sync server it names a different host than the - // desktop app meant; syncId is the same string on both sides. - resolvedHostData = (hostSyncId - ? await resolveHostBySyncId(hostSyncId, userId) - : await resolveHostById(id, userId)) as unknown as - typeof resolvedHostData | null; - - if (hostSyncId && !resolvedHostData) { - sshLogger.error( - "Refusing to connect: host is not known to this server", - undefined, - { - operation: "ssh_connect_host_sync_id_unknown", - hostId: id, - userId, - }, - ); - ws.send( - JSON.stringify({ - type: "error", - message: HOST_NOT_ON_THIS_SERVER_MESSAGE, - }), - ); - cleanupAuthState(connectionTimeout); - return; - } - - // Older clients send only the numeric id, which cannot be trusted to - // mean the same host here. Everything below is taken from the row it - // lands on -- the address, the credentials, the jump hosts, the stored - // host key -- so compare the address before using any of it. - if ( - !hostSyncId && - hostAddressMismatch(clientIp, resolvedHostData?.ip) - ) { - sshLogger.error( - "Refusing to connect: host id resolves to a different address here", - undefined, - { - operation: "ssh_connect_host_id_mismatch", - hostId: id, - userId, - clientIp, - resolvedIp: resolvedHostData?.ip, - }, - ); - ws.send( - JSON.stringify({ - type: "error", - message: HOST_ADDRESS_MISMATCH_MESSAGE, - }), - ); - cleanupAuthState(connectionTimeout); - return; - } - - if (resolvedHostData) { - const resolvedJumpHosts = resolveServerJumpHosts( - hostConfig.jumpHosts, - resolvedHostData.jumpHosts, - hostSyncId, - ); - if (resolvedJumpHosts !== hostConfig.jumpHosts) { - hostConfig.jumpHosts = resolvedJumpHosts; - sendLog( - "jump", - "info", - `Loaded ${resolvedJumpHosts?.length ?? 0} jump host(s) from server-side host data`, - ); - } - - if (resolvedHostData.useSocks5) { - hostConfig.useSocks5 = resolvedHostData.useSocks5; - hostConfig.socks5Host = resolvedHostData.socks5Host; - hostConfig.socks5Port = resolvedHostData.socks5Port; - hostConfig.socks5Username = resolvedHostData.socks5Username; - hostConfig.socks5Password = resolvedHostData.socks5Password; - hostConfig.socks5ProxyChain = resolvedHostData.socks5ProxyChain; - } - - if (!hostConfig.terminalConfig && resolvedHostData.terminalConfig) { - hostConfig.terminalConfig = resolvedHostData.terminalConfig; - } - - if ( - (!hostConfig.portKnockSequence || - hostConfig.portKnockSequence.length === 0) && - resolvedHostData.portKnockSequence && - resolvedHostData.portKnockSequence.length > 0 - ) { - hostConfig.portKnockSequence = resolvedHostData.portKnockSequence; - sendLog( - "port_knock", - "info", - `Loaded ${resolvedHostData.portKnockSequence.length} port knock(s) from server-side host data`, - ); - } - } - } catch (error) { - sshLogger.warn(`Failed to resolve server-side host data for ${id}`, { - operation: "ssh_host_data", - hostId: id, - error: getErrorMessage(error), - }); - } - } - - const serverHostId = resolveServerHostId(id, resolvedHostData); - - // Resolve credentials server-side when frontend doesn't provide them - let resolvedCredentials = { - username, - password, - key, - keyPassword, - keyType, - authType, - certPublicKey: undefined as string | undefined, - }; - const authMethodNotAvailable = false; - if (id && userId && !password && !key) { - try { - if (resolvedHostData) { - ip = resolvedHostData.ip || ip; - port = resolvedHostData.port || port; - username = resolvedHostData.username || username; - resolvedCredentials = { - username: resolvedHostData.username || username, - password: resolvedHostData.password, - key: resolvedHostData.key, - keyPassword: keyPassword || resolvedHostData.keyPassword, - keyType: resolvedHostData.keyType, - authType: resolvedHostData.authType, - certPublicKey: resolvedHostData.certPublicKey as string | undefined, - }; - sendLog( - "auth", - "info", - "Credentials resolved from server-side host data", - ); - } - } catch (error) { - sshLogger.warn(`Failed to resolve host credentials for ${id}`, { - operation: "ssh_credentials", - hostId: id, - error: getErrorMessage(error), - }); - } - } else if (credentialId && id && userId) { - try { - if (resolvedHostData) { - ip = resolvedHostData.ip || ip; - port = resolvedHostData.port || port; - username = resolvedHostData.username || username; - resolvedCredentials = { - username: resolvedHostData.username || username, - password: resolvedHostData.password, - key: resolvedHostData.key, - // Preserve user-supplied keyPassword (e.g. from passphrase dialog) over the empty DB value - keyPassword: keyPassword || resolvedHostData.keyPassword, - keyType: resolvedHostData.keyType, - authType: resolvedHostData.authType, - certPublicKey: resolvedHostData.certPublicKey as string | undefined, - }; - } - } catch (error) { - sshLogger.warn(`Failed to resolve credentials for host ${id}`, { - operation: "ssh_credentials", - hostId: id, - credentialId, - error: getErrorMessage(error), - }); - } - } - - if (hostConfig.passwordFallbackOnly && resolvedCredentials.password) { - resolvedCredentials = { - ...resolvedCredentials, - key: undefined, - keyPassword: undefined, - keyType: undefined, - certPublicKey: undefined, - authType: "password", - }; - } - - const connectsViaJumpHosts = !!( - hostConfig.jumpHosts && - hostConfig.jumpHosts.length > 0 && - hostConfig.userId - ); - - let connectHost = ip; - if (connectsViaJumpHosts) { - // The target is only reachable through the jump host's network (e.g. a - // VPN-only address), so DNS must be resolved there, not on this host. - sendLog( - "dns", - "info", - `Skipping local address resolution of ${ip} (resolved by jump host)`, - ); - } else { - sendLog("dns", "info", `Starting address resolution of ${ip}`); - try { - const resolution = await resolveHostForSshConnect(ip); - connectHost = resolution.host; - if (resolution.resolvedAddress && resolution.resolvedAddress !== ip) { - sendLog( - "dns", - "success", - `Resolved ${ip} to ${resolution.resolvedAddress}`, - { attempts: resolution.attempts }, - ); - } - } catch (error) { - const message = getErrorMessage(error); - sshLogger.error("SSH hostname resolution failed", error, { - operation: "terminal_dns_resolve", - hostId: id, - ip, - port, - transient: isRetriableDnsError(error), - }); - sendLog("dns", "error", `DNS resolution failed for ${ip}: ${message}`); - ws.send( - JSON.stringify({ - type: "error", - message: isRetriableDnsError(error) - ? "SSH error: DNS lookup temporarily failed. Check the Docker/container DNS configuration or try again." - : "SSH error: Could not resolve hostname from the Termix server container.", - }), - ); - cleanupAuthState(connectionTimeout); - return; - } - } - sendLog("tcp", "info", `Connecting to ${ip} port ${port}`); - - // A provider can hold the handshake open pending an out-of-band step - // (Tailscale SSH check mode delivers its re-auth URL as an auth banner, - // then blocks while the user logs in via the browser). - sshConn.on("banner", (banner: string) => { - if (!connectProvider?.onBanner || !connectTarget || !connectEnv) return; - const decision = connectProvider.onBanner( - banner, - connectTarget, - connectEnv, - ); - if (!decision) return; - - const authType = connectProvider.type; - - if (decision.action === "hold") { - bannerHoldPending = true; - clearTimeout(connectionTimeout); - connectionTimeout = setTimeout(onConnectionTimeout, decision.timeoutMs); - sendLog("auth", "info", decision.message); - ws.send( - JSON.stringify({ - type: `${authType}_check_required`, - hostId: id, - ...decision.details, - }), - ); - return; - } - - if (bannerHoldPending) { - bannerHoldPending = false; - sendLog("auth", "info", `${authType} check completed`); - ws.send( - JSON.stringify({ - type: `${authType}_check_completed`, - hostId: id, - ...decision.details, - }), - ); - } - }); - - sshConn.on("ready", () => { - clearOnlineStatus ??= hostSessionStatus.register(serverHostId); - clearTimeout(connectionTimeout); - isAuthRetrying = false; - if (bannerHoldPending) { - bannerHoldPending = false; - ws.send( - JSON.stringify({ - type: `${connectProvider?.type}_check_completed`, - hostId: id, - }), - ); - } - sshLogger.success("SSH connection established", { - operation: "terminal_ssh_connected", - sessionId, - userId, - hostId: id, - ip, - }); - - logAudit({ - userId, - username: userId, - action: "ssh_connect", - resourceType: "host", - resourceId: String(id), - resourceName: `${username}@${ip}:${port}`, - success: true, - }); - if (totpPromptSent) { - authLogger.success("TOTP verification successful for SSH session", { - operation: "terminal_totp_success", - sessionId, - userId, - hostId: id, - }); - } - sendLog("handshake", "success", "SSH handshake completed"); - sendLog("auth", "success", `Authentication successful for ${username}`); - sendLog("connected", "success", "Connection established"); - - const hostDisplayName = `${username}@${ip}:${port}`; - const tabInstanceId = hostConfig.instanceId; - const sessionLoggingEnabled = - resolvedHostData?.enableSessionLogging ?? - hostConfig.enableSessionLogging ?? - true; - currentSessionId = sessionManager.createSession( - userId, - id, - hostDisplayName, - data.cols, - data.rows, - tabInstanceId, - sessionLoggingEnabled, - ); - - // If createSession returned an existing live session (duplicate tabInstanceId), - // close the newly-established SSH connection and attach this WS to the live session instead. - const existingSession = sessionManager.getSession(currentSessionId); - if ( - existingSession && - existingSession.sshStream && - !existingSession.sshStream.destroyed && - existingSession.sshConn !== sshConn - ) { - const reusedSessionId = currentSessionId; - sshLogger.info( - "Reusing existing live session after duplicate connectToHost, closing new SSH conn", - { - operation: "terminal_reuse_existing_session", - sessionId: reusedSessionId, - tabInstanceId, - userId, - }, - ); - // Null out currentSessionId before ending the duplicate connection so - // the sshConn "close" handler does not destroy the reused session. - // Set isDuplicateConnDiscarded so the close handler does not send a - // "disconnected" message to the new WS that is now attached to the live session. - // Null out currentSessionId before ending the duplicate connection so - // the sshConn "close" handler does not destroy the reused session. - // Set isDuplicateConnDiscarded so the close handler exits without - // sending a "disconnected" message to the new WS. - currentSessionId = null; - isDuplicateConnDiscarded = true; - clearTimeout(connectionTimeout); - try { - sshConn?.end(); - } catch { - /* ignore */ - } - sshConn = null; - sshStream = null; - - // Point this WS handler's closure at the live session so the input - // handler can forward keystrokes via currentSessionId. - currentSessionId = reusedSessionId; - sshStream = existingSession.sshStream; - sshConn = existingSession.sshConn; - isConnecting = false; - isConnected = true; - sessionManager.attachWs(reusedSessionId, userId, ws, tabInstanceId); - - const buffered = sessionManager.getBuffer(existingSession); - if (buffered) { - ws.send(JSON.stringify({ type: "data", data: buffered })); - } - ws.send( - JSON.stringify({ - type: "sessionCreated", - sessionId: reusedSessionId, - }), - ); - ws.send( - JSON.stringify({ - type: "sessionAttached", - sessionId: reusedSessionId, - }), - ); - ws.send( - JSON.stringify({ type: "connected", message: "Session reattached" }), - ); - return; - } - - sshLogger.info("Terminal session created after SSH ready", { - operation: "terminal_session_created", - sessionId: currentSessionId, - userId, - hostId: id, - tabInstanceId, - ip, - port, - }); - - const conn = sshConn; - - if (!conn || isCleaningUp || !sshConn) { - sshLogger.warn( - "SSH connection was cleaned up before shell could be created", - { - operation: "ssh_shell", - hostId: id, - ip, - port, - username, - isCleaningUp, - connNull: !conn, - sshConnNull: !sshConn, - }, - ); - ws.send( - JSON.stringify({ - type: "error", - message: - "SSH connection was closed before terminal could be created", - }), - ); - if (currentSessionId) { - sessionManager.destroySession(currentSessionId); - currentSessionId = null; - } - cleanupAuthState(connectionTimeout); - return; - } - - isShellInitializing = true; - isConnecting = false; - isConnected = true; - - if (!sshConn) { - sshLogger.error( - "SSH connection became null right before shell creation", - { - operation: "ssh_shell", - hostId: id, - }, - ); - ws.send( - JSON.stringify({ - type: "error", - message: "SSH connection lost during setup", - }), - ); - isShellInitializing = false; - if (currentSessionId) { - sessionManager.destroySession(currentSessionId); - currentSessionId = null; - } - cleanupAuthState(connectionTimeout); - return; - } - - sshLogger.info("Creating shell", { - operation: "ssh_shell_start", - hostId: id, - ip, - port, - username, - }); - - let shellCallbackReceived = false; - const shellTimeout = setTimeout(() => { - if (!shellCallbackReceived && isShellInitializing) { - sshLogger.error("Shell creation timeout - no response from server", { - operation: "ssh_shell_timeout", - hostId: id, - ip, - port, - username, - }); - isShellInitializing = false; - ws.send( - JSON.stringify({ - type: "error", - message: - "Shell creation timeout. The server may not support interactive shells or the connection was interrupted.", - }), - ); - if (currentSessionId) { - sessionManager.destroySession(currentSessionId); - currentSessionId = null; - } - cleanupAuthState(connectionTimeout); - } - }, 15000); - - conn.shell( - { - rows: data.rows, - cols: data.cols, - term: "xterm-256color", - } as PseudoTtyOptions, - (err, stream) => { - shellCallbackReceived = true; - clearTimeout(shellTimeout); - isShellInitializing = false; - - if (err) { - sshLogger.error("Shell error", err, { - operation: "ssh_shell", - hostId: id, - ip, - port, - username, - }); - ws.send( - JSON.stringify({ - type: "error", - message: "Shell error: " + err.message, - }), - ); - if (currentSessionId) { - sessionManager.destroySession(currentSessionId); - currentSessionId = null; - } - cleanupAuthState(connectionTimeout); - return; - } - - sshStream = stream; - sshLogger.success("Terminal shell channel opened", { - operation: "terminal_shell_opened", - sessionId, - userId, - hostId: id, - termType: "xterm-256color", - }); - - if (currentSessionId) { - sessionManager.setSSHState( - currentSessionId, - sshConn!, - stream, - lastJumpClient, - ); - sessionManager.attachWs(currentSessionId, userId, ws); - - ws.send( - JSON.stringify({ - type: "sessionCreated", - sessionId: currentSessionId, - }), - ); - - sshLogger.info("Session ready for persistence", { - operation: "session_ready", - sessionId: currentSessionId, - userId, - hostId: id, - }); - } - - const boundSessionId = currentSessionId; - // A single TCP/SSH packet boundary can split a multi-byte UTF-8 - // character (e.g. the box-drawing glyphs mc/htop use for borders). - // Buffer.toString("utf-8") on each chunk independently replaces the - // split bytes with U+FFFD, which shows up as corrupted/inserted - // characters. StringDecoder carries incomplete trailing bytes over - // to the next chunk so multi-byte characters decode correctly. - const decoder = new StringDecoder("utf-8"); - - stream.on("data", (data: Buffer) => { - try { - const utf8String = decoder.write(data); - - if (!utf8String) return; - - const session = sessionManager.getSession(boundSessionId); - if (session) { - sessionManager.bufferOutput(boundSessionId!, utf8String); - sessionManager.broadcast(boundSessionId!, { - type: "data", - data: utf8String, - }); - } - } catch (error) { - sshLogger.error("Error encoding terminal data", error, { - operation: "terminal_data_encoding", - hostId: id, - dataLength: data.length, - }); - const fallback = data.toString("latin1"); - const session = sessionManager.getSession(boundSessionId); - if (session) { - sessionManager.bufferOutput(boundSessionId!, fallback); - sessionManager.broadcast(boundSessionId!, { - type: "data", - data: fallback, - }); - } - } - }); - - stream.on("close", (code: number | null) => { - const session = sessionManager.getSession(boundSessionId); - if (session) { - if (code != null) { - sessionManager.broadcast(boundSessionId!, { - type: "session_ended", - code, - }); - } else { - sessionManager.broadcast(boundSessionId!, { - type: "disconnected", - message: "Connection lost", - graceful: true, - }); - } - } - if (boundSessionId) { - sessionManager.destroySession(boundSessionId); - if (currentSessionId === boundSessionId) { - currentSessionId = null; - } - } - }); - - stream.on("error", (err: Error) => { - sshLogger.error("SSH stream error", err, { - operation: "ssh_stream", - hostId: id, - ip, - port, - username, - }); - const session = sessionManager.getSession(boundSessionId); - if (session) { - sessionManager.broadcast(boundSessionId!, { - type: "error", - message: "SSH stream error: " + err.message, - }); - } - }); - - const autoTmux = hostConfig.terminalConfig?.autoTmux === true; - - // Helper to run initialPath/executeCommand after the shell - // (or tmux session) is ready - const runPostShellCommands = (delay: number) => { - setTimeout(() => { - if (initialPath && initialPath.trim() !== "") { - let cdCommand: string; - if (isWindowsSftpPath(initialPath)) { - const winPath = sftpPathToLocalPath(initialPath); - const escaped = winPath.replace(/"/g, '""'); - cdCommand = `cd "${escaped}"\r`; - } else { - cdCommand = `cd "${initialPath.replace(/"/g, '\\"')}"\r`; - } - stream.write(cdCommand); - } - if (executeCommand && executeCommand.trim() !== "") { - setTimeout(() => { - stream.write(`${executeCommand}\r`); - }, 300); - } - }, delay); - }; - - if (tmuxAttachSession && conn) { - // Direct attach (tmux monitor): the session is known to exist, so - // skip detection and reuse the same path as the manual - // "tmux_attach" websocket message. - attachOrCreateTmuxSession(stream, tmuxAttachSession); - { - const session = sessionManager.getSession(boundSessionId); - if (session) session.tmuxSessionName = tmuxAttachSession; - } - sshLogger.info("Attached to requested tmux session", { - operation: "tmux_direct_attach", - sessionName: tmuxAttachSession, - hostId: id, - }); - ws.send( - JSON.stringify({ - type: "tmux_session_attached", - sessionName: tmuxAttachSession, - }), - ); - } else if (autoTmux && conn) { - (async () => { - try { - const detection = await detectTmux(conn); - if (!detection.available) { - sshLogger.warn("tmux not found on remote host", { - operation: "tmux_detection", - hostId: id, - }); - ws.send( - JSON.stringify({ - type: "tmux_unavailable", - message: - "tmux is not installed on the remote host. Falling back to standard shell.", - }), - ); - runPostShellCommands(0); - } else if (detection.sessions.length === 0) { - const newName = `termix-${id}-${Date.now().toString(36).slice(-4)}`; - attachOrCreateTmuxSession(stream, undefined, newName); - const confirmed = await waitForTmuxSession(conn, newName); - const session = sessionManager.getSession(boundSessionId); - if (session) { - session.tmuxSessionName = confirmed; - } - sshLogger.info("Created new tmux session", { - operation: "tmux_new_session", - sessionName: confirmed, - hostId: id, - }); - ws.send( - JSON.stringify({ - type: "tmux_session_created", - sessionName: confirmed, - }), - ); - runPostShellCommands(0); - } else { - sshLogger.info( - "Multiple tmux sessions found, sending list to frontend", - { - operation: "tmux_sessions_available", - sessions: detection.sessions, - hostId: id, - }, - ); - ws.send( - JSON.stringify({ - type: "tmux_sessions_available", - sessions: detection.sessions, - }), - ); - // Commands deferred until user picks a session - } - } catch (error) { - sshLogger.error("tmux detection failed", error, { - operation: "tmux_detection_error", - hostId: id, - }); - // Fallback: run commands in plain shell - runPostShellCommands(0); - } - })(); - } else { - // No tmux -- run commands directly as before - runPostShellCommands(0); - } - - ws.send( - JSON.stringify({ type: "connected", message: "SSH connected" }), - ); - - if (id && hostConfig.userId) { - dispatchLoginEvent( - id, - hostConfig.userId, - username, - getClientIp(req), - ).catch(() => {}); - } - - if (id && hostConfig.userId) { - (async () => { - try { - const host = - await createCurrentHostResolutionRepository().findHostById( - id, - hostConfig.userId!, - ); - - const hostName = - host?.userId === hostConfig.userId && host.name - ? host.name - : `${username}@${ip}:${port}`; - - await axios.post( - "http://localhost:30006/activity/log", - { - type: "terminal", - hostId: id, - hostName, - }, - { - headers: { - Authorization: `Bearer ${await authManager.generateJWTToken(hostConfig.userId!)}`, - }, - }, - ); - } catch (error) { - sshLogger.warn("Failed to log terminal activity", { - operation: "activity_log_error", - userId: hostConfig.userId, - hostId: id, - error: getErrorMessage(error), - }); - } - })(); - } - }, - ); - }); - - sshConn.on("error", (err: Error) => { - clearTimeout(connectionTimeout); - - sendLog("error", "error", `Connection error: ${err.message}`); - - sshLogger.error("SSH connection error", err, { - operation: "ssh_connect", - hostId: id, - ip, - port, - username, - authType: resolvedCredentials.authType, - warpgateAuthPromptSent, - isKeyboardInteractive, - hasKeyboardInteractiveFinish: !!keyboardInteractiveFinish, - keyboardInteractiveResponded, - }); - - // The provider decides what an auth failure means: clear a cached - // certificate and ask for a new sign-in, retry once, or give up with - // its own message. - const authFollowUp = - connectProvider?.onAuthFailed && connectEnv && connectTarget - ? connectProvider.onAuthFailed(connectTarget, connectEnv, { - error: err, - retries: authRetries, - canRetry: !connectConfig.sock && !bannerHoldPending, - methodNotAvailable: authMethodNotAvailable, - }) - : undefined; - - if (authFollowUp?.status === "interaction-required") { - if (currentSessionId) { - sessionManager.destroySession(currentSessionId); - currentSessionId = null; - } - cleanupAuthState(connectionTimeout); - sendLog("auth", "error", authFollowUp.message); - ws.send( - JSON.stringify({ - type: `${authFollowUp.interaction}_auth_required`, - hostId: id, - message: authFollowUp.message, - }), - ); - return; - } - - if ( - err.message.includes("Cannot parse privateKey") && - err.message.includes("no passphrase") - ) { - sendLog( - "auth", - "error", - "SSH key is encrypted but no passphrase was provided", - ); - isAwaitingAuthCredentials = true; - if (currentSessionId) { - sessionManager.destroySession(currentSessionId); - currentSessionId = null; - } - cleanupAuthState(connectionTimeout); - ws.send( - JSON.stringify({ - type: "passphrase_required", - message: - "The SSH key is encrypted. Please enter the passphrase to unlock it.", - }), - ); - return; - } - - if (authFollowUp?.status === "retry") { - authRetries++; - sendLog("auth", "info", authFollowUp.message); - sshLogger.info("Retrying SSH auth with provider changes", { - operation: "ssh_auth_retry", - hostId: id, - userId, - }); - - clearTimeout(connectionTimeout); - connectionTimeout = setTimeout( - onConnectionTimeout, - (connectConfig.readyTimeout as number | undefined) ?? 120000, - ); - - Object.assign(connectConfig, authFollowUp.patch); - - // ssh2's connect() ends an open socket and reconnects on close, keeping - // every listener attached, so the same client can be reused here. - isAuthRetrying = true; - sshConn.connect(connectConfig); - return; - } - - if (authFollowUp?.status === "error") { - sendLog("auth", "error", authFollowUp.message); - if (currentSessionId) { - sessionManager.destroySession(currentSessionId); - currentSessionId = null; - } - cleanupAuthState(connectionTimeout); - ws.send( - JSON.stringify({ type: "error", message: authFollowUp.message }), - ); - return; - } - - if ( - authMethodNotAvailable && - resolvedCredentials.authType === "none" && - !isKeyboardInteractive - ) { - sendLog( - "auth", - "error", - "Server does not support keyboard-interactive authentication", - ); - isAwaitingAuthCredentials = true; - if (currentSessionId) { - sessionManager.destroySession(currentSessionId); - currentSessionId = null; - } - cleanupAuthState(connectionTimeout); - ws.send( - JSON.stringify({ - type: "auth_method_not_available", - message: - "The server does not support keyboard-interactive authentication. Please provide credentials.", - }), - ); - return; - } - - if ( - resolvedCredentials.authType === "none" && - err.message.includes("All configured authentication methods failed") && - !isKeyboardInteractive && - !keyboardInteractiveResponded - ) { - isAwaitingAuthCredentials = true; - if (currentSessionId) { - sessionManager.destroySession(currentSessionId); - currentSessionId = null; - } - cleanupAuthState(connectionTimeout); - ws.send( - JSON.stringify({ - type: "auth_method_not_available", - message: - "The server does not support keyboard-interactive authentication. Please provide credentials.", - }), - ); - return; - } - - if ( - isKeyboardInteractive && - keyboardInteractiveFinish && - err.message.includes("All configured authentication methods failed") - ) { - sshLogger.warn( - "Authentication error during keyboard-interactive - SKIPPING cleanup, waiting for user response", - { - operation: "ssh_error_during_keyboard_interactive_skip_cleanup", - hostId: id, - error: err.message, - }, - ); - resetConnectionState(); - return; - } - - sshLogger.error("Proceeding with cleanup after error", { - operation: "ssh_error_cleanup", - hostId: id, - error: err.message, - }); - - if ( - err.message.includes("authentication") || - err.message.includes("Authentication") - ) { - authLogger.error("SSH authentication failed", err, { - operation: "terminal_ssh_auth_failed", - sessionId, - userId, - hostId: id, - authType: resolvedCredentials.authType, - }); - sendLog("auth", "error", `Authentication failed: ${err.message}`); - } else { - sendLog("error", "error", `Connection failed: ${err.message}`); - } - - let errorMessage = "SSH error: " + err.message; - if (err.message.includes("No matching key exchange algorithm")) { - errorMessage = - "SSH error: No compatible key exchange algorithm found. This may be due to an older SSH server or network device."; - } else if (err.message.includes("No matching cipher")) { - errorMessage = - "SSH error: No compatible cipher found. This may be due to an older SSH server or network device."; - } else if (err.message.includes("No matching MAC")) { - errorMessage = - "SSH error: No compatible MAC algorithm found. This may be due to an older SSH server or network device."; - } else if ( - err.message.includes("ENOTFOUND") || - err.message.includes("ENOENT") - ) { - errorMessage = - "SSH error: Could not resolve hostname or connect to server."; - } else if (err.message.includes("ECONNREFUSED")) { - errorMessage = - "SSH error: Connection refused. The server may not be running or the port may be incorrect."; - } else if (err.message.includes("ENETUNREACH")) { - const isIPv6 = ip && ip.includes(":"); - errorMessage = isIPv6 - ? "SSH error: Network unreachable. IPv6 may not be available in this environment. If running in Docker, enable IPv6 in the Docker daemon and network configuration." - : "SSH error: Network unreachable. Check your network configuration and routing."; - } else if (err.message.includes("ETIMEDOUT")) { - errorMessage = - "SSH error: Connection timed out. Check your network connection and server availability."; - } else if ( - err.message.includes("ECONNRESET") || - err.message.includes("EPIPE") - ) { - errorMessage = - "SSH error: Connection was reset. This may be due to network issues or server timeout."; - } else if ( - err.message.includes("authentication failed") || - err.message.includes("Permission denied") - ) { - errorMessage = - "SSH error: Authentication failed. Please check your username and password/key."; - } - - ws.send(JSON.stringify({ type: "error", message: errorMessage })); - if (currentSessionId) { - sessionManager.destroySession(currentSessionId); - currentSessionId = null; - } - cleanupAuthState(connectionTimeout); - }); - - sshConn.on("close", () => { - // The +password retry ends the socket before reconnecting; that close is - // part of the retry, not a disconnect. - if (isAuthRetrying) { - return; - } - - clearOnlineStatus?.(); - clearOnlineStatus = null; - - clearTimeout(connectionTimeout); - sshLogger.info("SSH connection closed", { - operation: "terminal_ssh_disconnected", - sessionId, - userId, - hostId: id, - }); - - if (isDuplicateConnDiscarded) { - cleanupAuthState(connectionTimeout); - return; - } - - if (isAwaitingAuthCredentials) { - if (currentSessionId) { - sessionManager.destroySession(currentSessionId); - currentSessionId = null; - } - cleanupAuthState(connectionTimeout); - return; - } - - if (isShellInitializing || (isConnected && !sshStream)) { - sshLogger.warn("SSH connection closed during shell initialization", { - operation: "ssh_close_during_init", - hostId: id, - ip, - port, - username, - isShellInitializing, - hasStream: !!sshStream, - }); - if (ws.readyState === WebSocket.OPEN) { - ws.send( - JSON.stringify({ - type: "error", - message: - "Connection closed during shell initialization. The server may have rejected the shell request.", - }), - ); - } - } else { - if (ws.readyState === WebSocket.OPEN) { - ws.send( - JSON.stringify({ - type: "disconnected", - message: "Connection closed", - }), - ); - } - } - if (currentSessionId) { - sessionManager.destroySession(currentSessionId); - currentSessionId = null; - } - cleanupAuthState(connectionTimeout); - }); - - const sshAuthManager = new SSHAuthManager({ - userId, - ws, - hostId: id || 0, - isKeyboardInteractive, - keyboardInteractiveResponded, - keyboardInteractiveFinish, - totpPromptSent, - warpgateAuthPromptSent, - totpTimeout, - warpgateAuthTimeout, - totpAttempts: 0, - }); - - sshConn.on( - "keyboard-interactive", - ( - name: string, - instructions: string, - instructionsLang: string, - prompts: Array<{ prompt: string; echo: boolean }>, - finish: (responses: string[]) => void, - ) => { - if (connectionTimeout) { - clearTimeout(connectionTimeout); - } - - sshAuthManager.handleKeyboardInteractive( - name, - instructions, - instructionsLang, - prompts, - finish, - resolvedCredentials as unknown as Parameters< - typeof sshAuthManager.handleKeyboardInteractive - >[5], - hostConfig, - ); - - isKeyboardInteractive = sshAuthManager.context.isKeyboardInteractive; - keyboardInteractiveResponded = - sshAuthManager.context.keyboardInteractiveResponded; - keyboardInteractiveFinish = - sshAuthManager.context.keyboardInteractiveFinish; - totpPromptSent = sshAuthManager.context.totpPromptSent; - warpgateAuthPromptSent = sshAuthManager.context.warpgateAuthPromptSent; - totpTimeout = sshAuthManager.context.totpTimeout; - warpgateAuthTimeout = sshAuthManager.context.warpgateAuthTimeout; - }, - ); - - const effectiveAuthType = - resolvedCredentials.authType || - (resolvedCredentials.key - ? "key" - : resolvedCredentials.password - ? "password" - : undefined); - if (!effectiveAuthType) { - sshLogger.error("No valid authentication method provided"); - ws.send( - JSON.stringify({ - type: "error", - message: "No valid authentication method provided", - }), - ); - return; - } - - connectTarget = { - ...(resolvedHostData ?? {}), - id: serverHostId ?? id, - ip, - port, - username, - userId: hostConfig.userId, - authType: effectiveAuthType, - password: resolvedCredentials.password, - key: resolvedCredentials.key, - keyPassword: resolvedCredentials.keyPassword, - keyType: resolvedCredentials.keyType, - certPublicKey: resolvedCredentials.certPublicKey, - forceKeyboardInteractive: hostConfig.forceKeyboardInteractive, - useWarpgate: hostConfig.useWarpgate, - terminalConfig: hostConfig.terminalConfig, - jumpHosts: hostConfig.jumpHosts, - useSocks5: hostConfig.useSocks5, - socks5Host: hostConfig.socks5Host, - socks5Port: hostConfig.socks5Port, - socks5Username: hostConfig.socks5Username, - socks5Password: hostConfig.socks5Password, - socks5ProxyChain: hostConfig.socks5ProxyChain, - portKnockSequence: hostConfig.portKnockSequence, - } as SshConnectHost; - - const built = await buildConnectConfig(connectTarget, { - userId, - purpose: "terminal", - client: sshConn, - serverHostId, - hostKeySocket: ws, - interactive: true, - log: (level, message) => sendLog("auth", level, message), - }); - const connectConfig = built.config; - // DNS was resolved above, and skipped when jump hosts do it instead. - connectConfig.host = connectHost; - connectProvider = built.provider; - connectEnv = built.env; - - if (built.outcome.status !== "ready") { - const outcome = built.outcome; - if (outcome.status === "interaction-required") { - ws.send( - JSON.stringify({ - type: `${outcome.interaction}_auth_required`, - hostId: id, - }), - ); - return; - } - if ( - outcome.status === "error" && - outcome.code === "passphrase-required" - ) { - sendLog( - "auth", - "error", - "SSH key is encrypted but no passphrase was provided", - ); - isAwaitingAuthCredentials = true; - cleanupAuthState(connectionTimeout); - ws.send( - JSON.stringify({ - type: "passphrase_required", - message: outcome.message, - }), - ); - return; - } - sshLogger.error("SSH auth could not be prepared", { - operation: "terminal_auth_prepare", - hostId: id, - authType: effectiveAuthType, - error: outcome.message, - }); - sendLog("auth", "error", outcome.message); - ws.send(JSON.stringify({ type: "error", message: outcome.message })); - return; - } - - if (hostConfig.terminalConfig?.agentForwarding) { - if (connectConfig.privateKey) { - try { - const parsed = ssh2Utils.parseKey( - connectConfig.privateKey as Buffer, - connectConfig.passphrase as string | undefined, - ); - if (parsed && !(parsed instanceof Error)) { - connectConfig.agent = new MemoryAgent(parsed); - connectConfig.agentForward = true; - sendLog("auth", "info", "SSH agent forwarding enabled"); - } - } catch { - sshLogger.warn("Failed to set up agent forwarding", { - operation: "agent_forward_setup", - hostId: id, - }); - } - } else if (connectConfig.agent) { - connectConfig.agentForward = true; - sendLog( - "auth", - "info", - "SSH agent forwarding enabled (external agent)", - ); - } - } - - const viaProxy = !!getHostSocks5Config(connectTarget); - - let transport: Awaited<ReturnType<typeof openSshTransport>>; - try { - transport = await openSshTransport(connectTarget, connectConfig, { - // Resolved above, or deliberately left to the jump host. - resolveDns: false, - log: (level, message) => sendLog("handshake", level, message), - }); - } catch (error) { - sshLogger.error("SSH transport failed", error, { - operation: "ssh_transport", - hostId: id, - ip, - port, - }); - ws.send( - JSON.stringify({ - type: "error", - message: - error instanceof JumpHostChainError - ? `Failed to connect through jump hosts: ${error.message}` - : getErrorMessage(error), - }), - ); - if (currentSessionId) { - sessionManager.destroySession(currentSessionId); - currentSessionId = null; - } - cleanupAuthState(connectionTimeout); - return; - } - lastJumpClient = transport.jumpClient; - - sendLog( - "handshake", - "info", - transport.via === "jump" - ? "Starting SSH session through jump host" + - (viaProxy ? " (via proxy)" : "") - : transport.via === "proxy" - ? "Starting SSH session (via proxy)" - : "Starting SSH session", - ); - sendLog("auth", "info", `Authenticating as ${username}`); - sshLogger.info("Initiating SSH connection", { - operation: "terminal_ssh_connect_attempt", - sessionId, - userId, - hostId: id, - ip, - port, - username, - authType: resolvedCredentials.authType, - via: transport.via, - }); - sshConn.connect(connectConfig); - } - - function handleResize(data: ResizeData) { - const cols = toTerminalDimension(data?.cols); - const rows = toTerminalDimension(data?.rows); - if (!cols || !rows) return; - - const resizeStream = - sessionManager.getSession(currentSessionId)?.sshStream ?? sshStream; - if (resizeStream && resizeStream.setWindow) { - resizeStream.setWindow(rows, cols, rows, cols); - const session = sessionManager.getSession(currentSessionId); - if (session) { - session.cols = cols; - session.rows = rows; - sessionManager.bufferResize(session.id, cols, rows); - } - ws.send(JSON.stringify({ type: "resized", cols, rows })); - } - } - - function cleanupAuthState(timeoutId?: NodeJS.Timeout) { - if (timeoutId) { - clearTimeout(timeoutId); - } - - if (totpTimeout) { - clearTimeout(totpTimeout); - totpTimeout = null; - } - - if (warpgateAuthTimeout) { - clearTimeout(warpgateAuthTimeout); - warpgateAuthTimeout = null; - } - - sshStream = null; - sshConn = null; - lastJumpClient = null; - - resetConnectionState(); - isCleaningUp = false; - isAwaitingAuthCredentials = false; - } - - // Note: PTY-level keepalive (writing \x00 to the stream) was removed. - // It was causing ^@ characters to appear in terminals with echoctl enabled. - // SSH-level keepalive is configured via connectConfig (keepaliveInterval, - // keepaliveCountMax, tcpKeepAlive), which handles connection health monitoring - // without producing visible output on the terminal. - // - // See: https://github.com/Termix-SSH/Support/issues/232 - // See: https://github.com/Termix-SSH/Support/issues/309 -}); diff --git a/src/backend/plugins/ctx-hosts.ts b/src/backend/plugins/ctx-hosts.ts index f28469f2a..87c9012c1 100644 --- a/src/backend/plugins/ctx-hosts.ts +++ b/src/backend/plugins/ctx-hosts.ts @@ -21,7 +21,9 @@ import type { PluginShareableRole, } from "@termix/plugin-sdk/backend"; import type { PluginManifest } from "@termix/plugin-sdk/manifest"; +import { PluginCapabilityError } from "@termix/plugin-sdk/backend"; import { assertCapability } from "./permissions.js"; +import { hostSessionStatus } from "../hosts/host-session-status.js"; import { getActor } from "./actor.js"; type AuditFn = ( @@ -392,5 +394,22 @@ export function createPluginHosts({ manifest, audit }: Deps): PluginHosts { .filter((r) => !r.isSystem) .map((r) => ({ id: r.id, name: r.name, displayName: r.displayName })); }, + + // Synchronous so a transport can call it from an ssh2 "ready" handler, so + // only the declaration is checked here, like ctx.http.router. + trackSession: (hostId: number) => { + if (!declared.includes("hosts:read")) { + throw new PluginCapabilityError(pluginId, "hosts:read"); + } + return hostSessionStatus.register(hostId); + }, + + recordActivity: async (hostId, type, hostName) => { + await requireRead(); + const userId = actingUser(); + const { recordRecentActivity } = + await import("../services/recent-activity.js"); + await recordRecentActivity(userId, { type, hostId, hostName }); + }, }; } diff --git a/src/backend/plugins/ctx-ssh-auth.ts b/src/backend/plugins/ctx-ssh-auth.ts index 6c6f76048..28de2f388 100644 --- a/src/backend/plugins/ctx-ssh-auth.ts +++ b/src/backend/plugins/ctx-ssh-auth.ts @@ -13,12 +13,16 @@ import type { PluginSshHost, } from "@termix/plugin-sdk/backend"; import type { PluginManifest } from "@termix/plugin-sdk/manifest"; -import { PluginCapabilityError } from "@termix/plugin-sdk/backend"; +import { + PluginCapabilityError, + PluginSshInteractionError, +} from "@termix/plugin-sdk/backend"; import { assertCapability } from "./permissions.js"; import { getActor } from "./actor.js"; import type { DisposableBag } from "./disposables.js"; import { getSshAuthProvider, + listSshAuthProviders, registerKeyboardInteractiveInterceptor, registerSshAuthProvider, } from "../hosts/connect/auth-provider-registry.js"; @@ -55,6 +59,7 @@ const PLUGIN_PURPOSES = new Set<SshConnectPurpose>([ "tunnel", "file-manager", "file-transfer", + "terminal", ]); function purposeOf(options?: PluginSshConnectOptions): SshConnectPurpose { @@ -222,6 +227,21 @@ export function createPluginSsh({ manifest, bag, audit }: Deps): PluginSsh { poolKey, + resolveHost: async (hostId, options) => { + await checkSsh(true); + const userId = actingUser(hostId); + const { resolveHostById, resolveHostBySyncId } = + await import("../hosts/host-resolver.js"); + const resolved = options?.syncId + ? await resolveHostBySyncId(options.syncId, userId) + : await resolveHostById(hostId, userId); + await audit("ssh_resolve_host", describeHost(hostId), { + success: !!resolved, + errorMessage: resolved ? undefined : "Host not found", + }); + return (resolved as unknown as PluginSshHost | null) ?? null; + }, + prepare: async (host, options) => { await checkSsh(true); const { buildConnectConfig } = @@ -232,22 +252,38 @@ export function createPluginSsh({ manifest, bag, audit }: Deps): PluginSsh { client: options.client as never, serverHostId: options.serverHostId, log: options.log, + interactive: options.interactive, + hostKeySocket: (options.hostKeySocket ?? null) as never, }); await audit("ssh_prepare", describeHost(host), { success: built.outcome.status === "ready", errorMessage: built.outcome.status === "ready" ? undefined : built.outcome.message, }); - return { config: built.config, outcome: built.outcome }; + const provider = built.provider; + const target = host as SshConnectHost; + return { + config: built.config, + outcome: built.outcome, + authType: provider?.type ?? null, + onBanner: provider?.onBanner + ? (banner: string) => + provider.onBanner!(banner, target, built.env) as never + : undefined, + onAuthFailed: provider?.onAuthFailed + ? (context) => provider.onAuthFailed!(target, built.env, context) + : undefined, + }; }, - openTransport: async (host, config) => { + openTransport: async (host, config, transportOptions) => { await checkSsh(false); const { openSshTransport } = await import("../hosts/connect/transport.js"); const opened = await openSshTransport( host as SshConnectHost, config as MutableConnectConfig, + transportOptions, ); if (opened.jumpClient) { const jumpClient = opened.jumpClient; @@ -261,6 +297,60 @@ export function createPluginSsh({ manifest, bag, audit }: Deps): PluginSsh { return opened; }, + startInteraction: async (interaction, request) => { + await checkSsh(true); + const userId = actingUser(request.hostId); + ensureCoreSshAuthProviders(); + const { createCurrentHostResolutionRepository } = + await import("../database/repositories/factory.js"); + const host = await createCurrentHostResolutionRepository().findHostById( + request.hostId, + userId, + ); + if (!host) throw new PluginSshInteractionError("Host not found"); + + const own = getSshAuthProvider(host.authType as string); + const provider = + own?.interaction === interaction && own.startInteraction + ? own + : listSshAuthProviders().find( + (candidate) => + candidate.interaction === interaction && + candidate.startInteraction, + ); + if (!provider?.startInteraction) { + throw new PluginSshInteractionError( + `No enabled provider handles ${interaction} sign-in`, + ); + } + await audit("ssh_start_interaction", describeHost(request.hostId), { + success: true, + }); + await provider.startInteraction({ + userId, + hostId: request.hostId, + host: { + name: host.name as string | null, + ip: host.ip as string, + username: host.username as string, + }, + socket: request.socket as never, + requestOrigin: request.requestOrigin, + payload: request.payload ?? {}, + }); + }, + + cancelInteraction: async (interaction, request) => { + await checkSsh(false); + const userId = actingUser(request.hostId ?? 0); + ensureCoreSshAuthProviders(); + for (const provider of listSshAuthProviders()) { + if (provider.interaction !== interaction) continue; + if (!provider.cancelInteraction) continue; + await provider.cancelInteraction({ userId, ...request }); + } + }, + classifyKeyboardInteractive: (round, host) => { ensureCoreSshAuthProviders(); return classifyKeyboardInteractive(round, host as SshConnectHost); diff --git a/src/backend/plugins/ctx.ts b/src/backend/plugins/ctx.ts index 48d781b5b..3ddb57f32 100644 --- a/src/backend/plugins/ctx.ts +++ b/src/backend/plugins/ctx.ts @@ -640,6 +640,29 @@ export function createPluginContext( openIsolatedWindow: (request) => desktopOpenIsolatedWindow(request), }, + audit: { + // Attribution comes from the runtime: the actor, never a plugin value. + record: async (entry) => { + try { + const { logAudit } = await import("../utils/audit-logger.js"); + const actor = getActor() ?? "system"; + await logAudit({ + userId: actor, + username: actor, + action: entry.action, + resourceType: entry.resourceType ?? "plugin", + resourceId: entry.resourceId ?? pluginId, + resourceName: entry.resourceName ?? manifest.name, + details: entry.details ?? `via plugin ${pluginId}`, + success: entry.success, + errorMessage: entry.errorMessage, + }); + } catch { + // Auditing must never break the caller. + } + }, + }, + /** * Background work acts as a named user. Always audited, because "this ran * as someone" is exactly the thing an operator needs to be able to see. diff --git a/src/backend/plugins/service-registry.ts b/src/backend/plugins/service-registry.ts index 97c4ed750..ea6b96fe2 100644 --- a/src/backend/plugins/service-registry.ts +++ b/src/backend/plugins/service-registry.ts @@ -166,6 +166,25 @@ export function getRegistration( return services.get(service); } +/** + * A plugin service as core itself calls it, or undefined while no plugin + * provides a compatible version. + * + * Core is not a plugin and already authorized the request it is serving, so + * this skips the per-call permission check a plugin's handle runs. It is the + * same object plugins get. Session sharing reaching the terminal's live + * sessions is the first caller. + */ +export function getServiceImplementation<T extends object>( + service: string, + versionRange: string, +): T | undefined { + const registration = services.get(service); + if (!registration) return undefined; + if (!semver.satisfies(registration.version, versionRange)) return undefined; + return registration.implementation as T; +} + export function listServices(): ServiceRegistration[] { return [...services.values()]; } diff --git a/src/backend/plugins/ws.ts b/src/backend/plugins/ws.ts index b8f50e38e..c2bf820bd 100644 --- a/src/backend/plugins/ws.ts +++ b/src/backend/plugins/ws.ts @@ -159,6 +159,8 @@ export async function handlePluginUpgrade( return true; } userId = resolved; + } else if (route.options.optionalAuth) { + userId = (await verifyToken(request)) ?? ""; } if (route.rawHandler) { @@ -175,6 +177,17 @@ export async function handlePluginUpgrade( return true; } + const { getClientIp, getRequestOrigin } = + await import("../utils/request-origin.js"); + const { DataCrypto } = await import("../utils/data-crypto.js"); + const connection = { + userId, + request, + clientIp: getClientIp(request as never), + requestOrigin: getRequestOrigin(request as never), + isDataUnlocked: () => !!userId && !!DataCrypto.getUserDataKey(userId), + }; + server.handleUpgrade(request, socket, head, (ws: WebSocket) => { const handler = route.handler; if (!handler) { @@ -182,7 +195,7 @@ export async function handlePluginUpgrade( return; } runAsActor(userId || "anonymous", "request", () => { - void Promise.resolve(handler({ userId, request, socket: ws })).catch( + void Promise.resolve(handler({ ...connection, socket: ws })).catch( (error) => { pluginLogger.error( `Plugin ${route.pluginId} socket ${route.path} failed`, diff --git a/src/backend/services/dashboard.ts b/src/backend/services/dashboard.ts index b600fa2fe..0722b8bfe 100644 --- a/src/backend/services/dashboard.ts +++ b/src/backend/services/dashboard.ts @@ -6,13 +6,9 @@ import { dashboardLogger } from "../utils/logger.js"; import { AuthManager } from "../utils/auth-manager.js"; import type { AuthenticatedRequest } from "../../types/index.js"; import { dashboardServiceLinksRouter } from "../database/routes/dashboard-service-links-routes.js"; -import { - createCurrentHostResolutionRepository, - createCurrentRbacAccessRepository, - createCurrentRecentActivityRepository, - createCurrentRoleRepository, -} from "../database/repositories/factory.js"; +import { createCurrentRecentActivityRepository } from "../database/repositories/factory.js"; import { DataCrypto } from "../utils/data-crypto.js"; +import { recordRecentActivity } from "./recent-activity.js"; import { listenOnServicePort } from "../utils/service-listen.js"; const app = express(); @@ -21,9 +17,6 @@ const authManager = AuthManager.getInstance(); const serverStartTime = Date.now(); -const activityRateLimiter = new Map<string, number>(); -const RATE_LIMIT_MS = 1000; - function isUserDataUnlocked(userId: string): boolean { return DataCrypto.getUserDataKey(userId) !== null; } @@ -181,91 +174,24 @@ app.post("/activity/log", async (req, res) => { }); } - if ( - ![ - "terminal", - "file_manager", - "server_stats", - "tunnel", - "docker", - "telnet", - "vnc", - "rdp", - ].includes(type) - ) { + const result = await recordRecentActivity(userId, { + type, + hostId, + hostName, + }); + if (result.status === "invalid_type") { return res.status(400).json({ error: "Invalid activity type. Must be 'terminal', 'file_manager', 'server_stats', 'tunnel', 'docker', 'telnet', 'vnc', or 'rdp'", }); } - - const rateLimitKey = `${userId}:${hostId}:${type}`; - const now = Date.now(); - const lastLogged = activityRateLimiter.get(rateLimitKey); - - if (lastLogged && now - lastLogged < RATE_LIMIT_MS) { + if (result.status === "rate_limited") { return res.json({ message: "Activity already logged recently (rate limited)", }); } - - activityRateLimiter.set(rateLimitKey, now); - - if (activityRateLimiter.size > 10000) { - const entriesToDelete: string[] = []; - for (const [key, timestamp] of activityRateLimiter.entries()) { - if (now - timestamp > RATE_LIMIT_MS * 2) { - entriesToDelete.push(key); - } - } - entriesToDelete.forEach((key) => activityRateLimiter.delete(key)); - } - - const isOwnedHost = - await createCurrentHostResolutionRepository().isHostOwnedByUser( - hostId, - userId, - ); - - if (!isOwnedHost) { - const roleIds = - await createCurrentRoleRepository().listUserRoleIds(userId); - const sharedHosts = - await createCurrentRbacAccessRepository().listVisibleHostAccessEntries( - userId, - roleIds, - ); - const hasSharedAccess = sharedHosts.some( - (access) => access.hostId === hostId, - ); - - if (!hasSharedAccess) { - return res - .status(404) - .json({ error: "Host not found or access denied" }); - } - } - - const result = await createCurrentRecentActivityRepository().create({ - userId, - type, - hostId, - hostName, - }); - - // Best-effort trim of old activity entries; failures here should not - // cause the primary /activity/log request to 500. - try { - await createCurrentRecentActivityRepository().trimUserActivity( - userId, - 100, - ); - } catch (trimErr) { - dashboardLogger.warn("Failed to trim recent_activity (non-fatal)", { - operation: "trim_recent_activity", - userId, - error: trimErr instanceof Error ? trimErr.message : String(trimErr), - }); + if (result.status === "denied") { + return res.status(404).json({ error: "Host not found or access denied" }); } res.json({ message: "Activity logged", id: result.id }); diff --git a/src/backend/services/recent-activity.ts b/src/backend/services/recent-activity.ts new file mode 100644 index 000000000..df231d4e2 --- /dev/null +++ b/src/backend/services/recent-activity.ts @@ -0,0 +1,93 @@ +import { + createCurrentHostResolutionRepository, + createCurrentRbacAccessRepository, + createCurrentRecentActivityRepository, + createCurrentRoleRepository, +} from "../database/repositories/factory.js"; +import { dashboardLogger } from "../utils/logger.js"; + +const RATE_LIMIT_MS = 1000; +const activityRateLimiter = new Map<string, number>(); + +export const RECENT_ACTIVITY_TYPES = [ + "terminal", + "file_manager", + "server_stats", + "tunnel", + "docker", + "telnet", + "vnc", + "rdp", +] as const; + +export type RecordActivityResult = + | { status: "logged"; id: number } + | { status: "rate_limited" } + | { status: "invalid_type" } + | { status: "denied" }; + +/** + * Adds one recent activity entry: the dashboard's /activity/log route and + * ctx.hosts.recordActivity both come through here, so the rate limit and the + * access rule are the same for both. + */ +export async function recordRecentActivity( + userId: string, + entry: { type: string; hostId: number; hostName: string }, +): Promise<RecordActivityResult> { + const { type, hostId, hostName } = entry; + if (!(RECENT_ACTIVITY_TYPES as readonly string[]).includes(type)) { + return { status: "invalid_type" }; + } + + const rateLimitKey = `${userId}:${hostId}:${type}`; + const now = Date.now(); + const lastLogged = activityRateLimiter.get(rateLimitKey); + if (lastLogged && now - lastLogged < RATE_LIMIT_MS) { + return { status: "rate_limited" }; + } + activityRateLimiter.set(rateLimitKey, now); + + if (activityRateLimiter.size > 10000) { + for (const [key, timestamp] of activityRateLimiter.entries()) { + if (now - timestamp > RATE_LIMIT_MS * 2) activityRateLimiter.delete(key); + } + } + + const isOwnedHost = + await createCurrentHostResolutionRepository().isHostOwnedByUser( + hostId, + userId, + ); + if (!isOwnedHost) { + const roleIds = await createCurrentRoleRepository().listUserRoleIds(userId); + const sharedHosts = + await createCurrentRbacAccessRepository().listVisibleHostAccessEntries( + userId, + roleIds, + ); + if (!sharedHosts.some((access) => access.hostId === hostId)) { + return { status: "denied" }; + } + } + + const result = await createCurrentRecentActivityRepository().create({ + userId, + type, + hostId, + hostName, + }); + + // Best-effort trim; a failure here must not fail the log itself. + try { + await createCurrentRecentActivityRepository().trimUserActivity(userId, 100); + } catch (trimErr) { + dashboardLogger.warn("Failed to trim recent_activity (non-fatal)", { + operation: "trim_recent_activity", + userId, + error: trimErr instanceof Error ? trimErr.message : String(trimErr), + }); + } + + return { status: "logged", id: result.id }; +} diff --git a/src/backend/starter.ts b/src/backend/starter.ts index cd758793c..2f754b96c 100644 --- a/src/backend/starter.ts +++ b/src/backend/starter.ts @@ -361,6 +361,10 @@ async function provisionLocalDesktopUserIfNeeded(): Promise<void> { const { runWebEndpointSettingsMigration } = await import("./utils/crypto-migration/web-endpoint-settings-migration.js"); await runWebEndpointSettingsMigration(); + + const { runSshTerminalSettingsMigration } = + await import("./utils/crypto-migration/ssh-terminal-settings-migration.js"); + await runSshTerminalSettingsMigration(); } catch (error) { systemLogger.warn("Plugin runtime failed to initialize", { operation: "plugin_init", diff --git a/src/backend/tests/database/repositories/command-history-repository.test.ts b/src/backend/tests/database/repositories/command-history-repository.test.ts deleted file mode 100644 index 83e821a9a..000000000 --- a/src/backend/tests/database/repositories/command-history-repository.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { afterEach, describe, expect, it } from "vitest"; -import { TestSqliteDatabase } from "./test-support.js"; -import { CommandHistoryRepository } from "../../../database/repositories/command-history-repository.js"; - -describe("CommandHistoryRepository", () => { - let adapter: TestSqliteDatabase | null = null; - - afterEach(async () => { - if (adapter) { - await adapter.close(); - adapter = null; - } - }); - - async function createRepository( - onWrite?: () => void | Promise<void>, - ): Promise<CommandHistoryRepository> { - 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-1', 'two', '10.0.0.1', 22, 'root', 'password'), (3, 'user-2', 'other', '10.0.0.1', 22, 'root', 'password'); - `); - - return new CommandHistoryRepository(context, onWrite); - } - - it("creates and lists unique commands by latest execution", async () => { - const repo = await createRepository(); - - await repo.create("user-1", 1, "ls", "2026-06-27T00:00:00.000Z"); - await repo.create("user-1", 1, "pwd", "2026-06-27T01:00:00.000Z"); - await repo.create("user-1", 1, "ls", "2026-06-27T02:00:00.000Z"); - await repo.create("user-2", 3, "whoami", "2026-06-27T03:00:00.000Z"); - - expect(await repo.listUniqueCommandsForHost("user-1", 1)).toEqual([ - "ls", - "pwd", - ]); - expect(await repo.listCommandsForHost("user-1", 1)).toEqual([ - "ls", - "pwd", - "ls", - ]); - }); - - it("deletes commands by command, host, host list, and user", async () => { - let writeCount = 0; - const repo = await createRepository(() => { - writeCount += 1; - }); - - await repo.create("user-1", 1, "ls"); - await repo.create("user-1", 1, "ls"); - await repo.create("user-1", 2, "pwd"); - await repo.create("user-2", 3, "whoami"); - expect(writeCount).toBe(4); - - expect(await repo.deleteCommandForHost("user-1", 1, "missing")).toBe(0); - expect(writeCount).toBe(4); - - expect(await repo.deleteCommandForHost("user-1", 1, "ls")).toBe(2); - expect(writeCount).toBe(5); - - expect(await repo.deleteByUserAndHost("user-1", 2)).toBe(1); - expect(await repo.deleteByHostIds([])).toBe(0); - expect(await repo.deleteByHostIds([3])).toBe(1); - expect(writeCount).toBe(7); - - await repo.create("user-1", 1, "date"); - expect(await repo.deleteByUserId("user-1")).toBe(1); - expect(writeCount).toBe(9); - }); -}); diff --git a/src/backend/tests/database/routes/terminal-image-storage-settings-route.test.ts b/src/backend/tests/database/routes/terminal-image-storage-settings-route.test.ts deleted file mode 100644 index 97c779cdb..000000000 --- a/src/backend/tests/database/routes/terminal-image-storage-settings-route.test.ts +++ /dev/null @@ -1,304 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { EventEmitter } from "events"; -import fs from "fs/promises"; -import os from "os"; -import path from "path"; -import express, { - type Request, - type RequestHandler, - type Response, -} from "express"; - -const state = vi.hoisted(() => ({ - userId: "admin-1", - settings: {} as Record<string, string>, - sessions: [] as unknown[], -})); - -vi.mock("../../../utils/logger.js", () => ({ - authLogger: { warn: vi.fn(), error: vi.fn(), info: vi.fn() }, - databaseLogger: { warn: vi.fn(), error: vi.fn(), info: vi.fn() }, -})); - -vi.mock("../../../hosts/terminal/session-manager.js", () => ({ - sessionManager: { - getUserSessions: () => state.sessions, - }, -})); - -vi.mock("../../../database/repositories/factory.js", () => ({ - createCurrentSettingsRepository: () => ({ - get: async (key: string) => state.settings[key] ?? null, - set: async (key: string, value: string) => { - state.settings[key] = value; - }, - setMany: async (writes: Array<{ key: string; value: string }>) => { - for (const write of writes) state.settings[write.key] = write.value; - }, - }), -})); - -const { registerUserImageStorageRoutes } = - await import("../../../database/routes/user-image-storage-routes.js"); - -const requireAdmin: RequestHandler = (_req, _res, next) => next(); -const router = express.Router(); -registerUserImageStorageRoutes(router, requireAdmin); - -interface RouteLayer { - route?: { - path: string; - methods: Record<string, boolean>; - stack: Array<{ handle: RequestHandler }>; - }; -} - -function handlerFor(pathName: string, method: string): RequestHandler { - const layer = (router as unknown as { stack: RouteLayer[] }).stack.find( - (candidate) => - candidate.route?.path === pathName && candidate.route.methods[method], - ); - if (!layer) throw new Error(`Route not registered: ${method} ${pathName}`); - return layer.route!.stack[layer.route!.stack.length - 1]!.handle; -} - -const getHandler = handlerFor("/terminal-image-storage-settings", "get"); -const patchHandler = handlerFor("/terminal-image-storage-settings", "patch"); -const testHandler = handlerFor("/terminal-image-storage-settings/test", "post"); - -async function invoke(handler: RequestHandler, body?: unknown) { - const req = { - userId: state.userId, - body: body ?? {}, - headers: {}, - } as unknown as Request; - const result = { statusCode: 200, body: null as unknown }; - const res = { - status(code: number) { - result.statusCode = code; - return this; - }, - json(payload: unknown) { - result.body = payload; - return this; - }, - } as unknown as Response; - await handler(req, res, () => {}); - return result; -} - -/** Fake ssh2 exec channel: every command exits 0. */ -function fakeSshConn() { - return { - exec( - _command: string, - callback: (error: Error | undefined, stream?: unknown) => void, - ) { - const stream = new EventEmitter() as EventEmitter & { - resume: () => void; - }; - stream.resume = () => queueMicrotask(() => stream.emit("close", 0)); - callback(undefined, stream); - }, - }; -} - -const LEGACY_ENV_NAMES = [ - "TERMIX_IMAGE_STORAGE_MODE", - "TERMIX_IMAGE_DIR", - "TERMIX_IMAGE_HOST_PATH", - "TERMIX_IMAGE_TTL_MS", - "TERMIX_MAX_IMAGE_COUNT", - "TERMIX_MAX_IMAGE_STORAGE_BYTES", - "DATA_DIR", -]; - -let savedEnv: Record<string, string | undefined>; -let localDir: string; - -beforeEach(async () => { - state.settings = {}; - state.sessions = []; - savedEnv = Object.fromEntries( - LEGACY_ENV_NAMES.map((name) => [name, process.env[name]]), - ); - for (const name of LEGACY_ENV_NAMES) delete process.env[name]; - // The settings validator rejects backslashes, so use a POSIX-style form of - // the real temp dir. Windows still resolves it, so file checks keep working. - localDir = ( - await fs.mkdtemp(path.join(os.tmpdir(), "termix-image-test-")) - ).replace(/\\/g, "/"); -}); - -afterEach(async () => { - for (const name of LEGACY_ENV_NAMES) { - if (savedEnv[name] === undefined) delete process.env[name]; - else process.env[name] = savedEnv[name]; - } - await fs.rm(localDir, { recursive: true, force: true }); -}); - -describe("GET /users/terminal-image-storage-settings", () => { - it("returns the public settings shape without the backend localDir", async () => { - state.settings["terminal_image_storage_mode"] = "local"; - state.settings["terminal_image_local_dir"] = localDir; - state.settings["terminal_image_host_path"] = "/mnt/images"; - - const response = await invoke(getHandler); - - expect(response.statusCode).toBe(200); - expect(response.body).toEqual({ - mode: "local", - hostPath: "/mnt/images", - ttlMs: 3_600_000, - maxCount: 100, - maxBytes: 5_368_709_120, - localMappingConfigured: true, - }); - expect(JSON.stringify(response.body)).not.toContain(localDir); - }); -}); - -describe("PATCH /users/terminal-image-storage-settings", () => { - it("rejects an invalid mode with a safe 400", async () => { - const response = await invoke(patchHandler, { mode: "nfs" }); - expect(response.statusCode).toBe(400); - expect(response.body).toEqual({ - error: "Invalid value for mode", - code: "IMAGE_STORAGE_SETTINGS_INVALID", - field: "mode", - }); - }); - - it("rejects a relative localDir", async () => { - const response = await invoke(patchHandler, { localDir: "images/tmp" }); - expect(response.statusCode).toBe(400); - expect(response.body).toMatchObject({ - code: "IMAGE_STORAGE_SETTINGS_INVALID", - field: "localDir", - }); - expect(JSON.stringify(response.body)).not.toContain("images/tmp"); - }); - - it("rejects out-of-range numeric limits", async () => { - const response = await invoke(patchHandler, { maxBytes: 1024 }); - expect(response.statusCode).toBe(400); - expect(response.body).toMatchObject({ - code: "IMAGE_STORAGE_SETTINGS_INVALID", - field: "maxBytes", - }); - }); - - it("rejects unknown fields without writing anything", async () => { - const response = await invoke(patchHandler, { shellPath: "/tmp/x" }); - expect(response.statusCode).toBe(400); - expect(response.body).toMatchObject({ - code: "IMAGE_STORAGE_SETTINGS_UNKNOWN_FIELD", - }); - expect(state.settings).toEqual({}); - }); - - // On Windows path.resolve turns the stored dir back into a backslash path, - // which the validator rejects on read, so the round trip only holds on POSIX. - it.skipIf(process.platform === "win32")( - "persists a valid partial update and returns the public shape", - async () => { - const response = await invoke(patchHandler, { - mode: "local", - localDir, - hostPath: "/host/images", - ttlMs: 60_000, - maxCount: 5, - maxBytes: 10_485_760, - }); - - expect(response.statusCode).toBe(200); - expect(state.settings).toEqual({ - terminal_image_storage_mode: "local", - terminal_image_local_dir: path.resolve(localDir), - terminal_image_host_path: "/host/images", - terminal_image_ttl_ms: "60000", - terminal_image_max_count: "5", - terminal_image_max_storage_bytes: "10485760", - }); - expect(response.body).toMatchObject({ - mode: "local", - hostPath: "/host/images", - ttlMs: 60_000, - maxCount: 5, - maxBytes: 10_485_760, - localMappingConfigured: true, - }); - expect(JSON.stringify(response.body)).not.toContain(localDir); - }, - ); -}); - -describe("POST /users/terminal-image-storage-settings/test", () => { - it("requires an instanceId", async () => { - const response = await invoke(testHandler, {}); - expect(response.statusCode).toBe(400); - expect(response.body).toMatchObject({ code: "IMAGE_SESSION_MISSING" }); - }); - - it("reports unavailable storage when no session is connected", async () => { - const response = await invoke(testHandler, { instanceId: "tab-1" }); - expect(response.statusCode).toBe(200); - expect(response.body).toEqual({ - mode: "auto", - connected: false, - remoteSftpAvailable: false, - localHostVisible: null, - selectedMode: "unavailable", - localMappingConfigured: false, - }); - }); - - it("probes local visibility through the connected session only", async () => { - state.settings["terminal_image_local_dir"] = localDir; - state.settings["terminal_image_host_path"] = "/host/images"; - state.sessions = [ - { - tabInstanceId: "tab-1", - isConnected: true, - sshConn: fakeSshConn(), - }, - ]; - - const response = await invoke(testHandler, { instanceId: "tab-1" }); - - expect(response.statusCode).toBe(200); - expect(response.body).toEqual({ - mode: "auto", - connected: true, - remoteSftpAvailable: true, - localHostVisible: true, - selectedMode: "local", - localMappingConfigured: true, - }); - // The bounded probe cleans up after itself. - expect( - (await fs.readdir(localDir)).filter((f) => f.includes("probe")), - ).toEqual([]); - }); - - it("does not probe sessions owned by other instance IDs", async () => { - state.sessions = [ - { - tabInstanceId: "tab-2", - isConnected: true, - sshConn: fakeSshConn(), - }, - ]; - - const response = await invoke(testHandler, { instanceId: "tab-1" }); - - expect(response.statusCode).toBe(200); - expect(response.body).toMatchObject({ - connected: false, - remoteSftpAvailable: false, - localHostVisible: null, - selectedMode: "unavailable", - }); - }); -}); diff --git a/src/backend/tests/database/routes/terminal-image-upload-route.test.ts b/src/backend/tests/database/routes/terminal-image-upload-route.test.ts deleted file mode 100644 index 8f9c2ea22..000000000 --- a/src/backend/tests/database/routes/terminal-image-upload-route.test.ts +++ /dev/null @@ -1,541 +0,0 @@ -import { - afterEach, - beforeAll, - beforeEach, - describe, - expect, - it, - vi, -} from "vitest"; -import { EventEmitter } from "events"; -import fs from "fs/promises"; -import os from "os"; -import path from "path"; -import { randomUUID } from "crypto"; -import sharp from "sharp"; -import type { Request, RequestHandler, Response } from "express"; -import type { ImageSftpClient } from "../../../database/routes/terminal-image-storage.js"; -import { databaseLogger } from "../../../utils/logger.js"; - -const state = vi.hoisted(() => ({ - userId: "user-1", - settings: {} as Record<string, string>, - sessions: [] as unknown[], -})); - -vi.mock("../../../utils/logger.js", () => ({ - authLogger: { warn: vi.fn(), error: vi.fn(), info: vi.fn() }, - databaseLogger: { warn: vi.fn(), error: vi.fn(), info: vi.fn() }, -})); - -vi.mock("../../../utils/auth-manager.js", () => ({ - AuthManager: { - getInstance: () => ({ - createAuthMiddleware: - () => (_req: unknown, _res: unknown, next: () => void) => - next(), - createDataAccessMiddleware: - () => (_req: unknown, _res: unknown, next: () => void) => - next(), - }), - }, -})); - -vi.mock("../../../hosts/terminal/session-manager.js", () => ({ - sessionManager: { - getUserSessions: () => state.sessions, - }, -})); - -vi.mock("../../../database/repositories/factory.js", () => ({ - createCurrentSettingsRepository: () => ({ - get: async (key: string) => state.settings[key] ?? null, - }), - createCurrentHostResolutionRepository: () => ({}), - createCurrentCommandHistoryRepository: () => ({}), -})); - -const { default: router } = - await import("../../../database/routes/terminal.js"); - -interface RouteLayer { - route?: { - path: string; - methods: Record<string, boolean>; - stack: Array<{ handle: RequestHandler }>; - }; -} - -const imageUploadLayer = ( - router as unknown as { stack: RouteLayer[] } -).stack.find( - (layer) => layer.route?.path === "/image-upload" && layer.route.methods.post, -); -const imageUploadHandler = - imageUploadLayer!.route!.stack[imageUploadLayer!.route!.stack.length - 1]! - .handle; - -function fakeSftp(behavior: { writeError?: Error } = {}): { - sftp: ImageSftpClient; - written: Map<string, Buffer>; - end: ReturnType<typeof vi.fn>; -} { - const written = new Map<string, Buffer>(); - const end = vi.fn(); - const sftp = { - mkdir: ( - _dir: string, - attrsOrCallback: { mode?: number } | ((err?: Error) => void), - maybeCallback?: (err?: Error) => void, - ) => { - const callback = - typeof attrsOrCallback === "function" - ? attrsOrCallback - : maybeCallback!; - callback(); - }, - createWriteStream: (remotePath: string, _options?: { mode?: number }) => { - const stream = new EventEmitter() as NodeJS.WritableStream & { - end: (data: Buffer) => void; - }; - stream.end = (data: Buffer) => { - queueMicrotask(() => { - if (behavior.writeError) { - stream.emit("error", behavior.writeError); - return; - } - written.set(remotePath, data); - stream.emit("close"); - }); - }; - return stream; - }, - readdir: ( - _dir: string, - callback: ( - error: Error | undefined, - entries: Array<{ - filename: string; - attrs?: { size?: number; mtime?: number }; - }>, - ) => void, - ) => callback(undefined, []), - unlink: (_path: string, callback: (error?: Error) => void) => callback(), - rmdir: (_dir: string, callback: (error?: Error) => void) => callback(), - end, - } as unknown as ImageSftpClient & { end: ReturnType<typeof vi.fn> }; - return { sftp, written, end }; -} - -function connectedSession(instanceId: string, sftp: ImageSftpClient) { - return { - tabInstanceId: instanceId, - isConnected: true, - sshConn: { - sftp: ( - callback: (err: Error | undefined, sftp: ImageSftpClient) => void, - ) => callback(undefined, sftp), - }, - }; -} - -async function invoke(options: { - file?: { buffer: Buffer; mimetype: string; size: number }; - instanceId?: string; - metadata?: { source?: string; clientUploadTimestamp?: string }; -}) { - const body: Record<string, string> = {}; - if (options.instanceId) body.instanceId = options.instanceId; - if (options.metadata?.source !== undefined) - body.source = options.metadata.source; - if (options.metadata?.clientUploadTimestamp !== undefined) - body.clientUploadTimestamp = options.metadata.clientUploadTimestamp; - const req = { - userId: state.userId, - body, - file: options.file, - headers: {}, - } as unknown as Request; - const result = { statusCode: 200, body: null as unknown }; - const res = { - status(code: number) { - result.statusCode = code; - return this; - }, - json(body: unknown) { - result.body = body; - return this; - }, - } as unknown as Response; - await imageUploadHandler(req, res, () => {}); - return result; -} - -const LEGACY_ENV_NAMES = [ - "TERMIX_IMAGE_STORAGE_MODE", - "TERMIX_IMAGE_DIR", - "TERMIX_IMAGE_HOST_PATH", - "TERMIX_IMAGE_TTL_MS", - "TERMIX_MAX_IMAGE_COUNT", - "TERMIX_MAX_IMAGE_STORAGE_BYTES", - "DATA_DIR", -]; - -let pngBuffer: Buffer; -let savedEnv: Record<string, string | undefined>; - -beforeAll(async () => { - pngBuffer = await sharp({ - create: { width: 2, height: 2, channels: 3, background: "#ffffff" }, - }) - .png() - .toBuffer(); -}); - -beforeEach(() => { - state.settings = {}; - state.sessions = []; - savedEnv = Object.fromEntries( - LEGACY_ENV_NAMES.map((name) => [name, process.env[name]]), - ); - for (const name of LEGACY_ENV_NAMES) delete process.env[name]; -}); - -afterEach(() => { - for (const name of LEGACY_ENV_NAMES) { - if (savedEnv[name] === undefined) delete process.env[name]; - else process.env[name] = savedEnv[name]; - } -}); - -describe("terminal image upload route", () => { - it("rejects requests without an image file", async () => { - const response = await invoke({ instanceId: "tab-1" }); - expect(response.statusCode).toBe(400); - expect(response.body).toMatchObject({ code: "IMAGE_FILE_MISSING" }); - }); - - it("requires a connected terminal in explicit remote-sftp mode", async () => { - state.settings["terminal_image_storage_mode"] = "remote-sftp"; - const response = await invoke({ - file: { - buffer: pngBuffer, - mimetype: "image/png", - size: pngBuffer.length, - }, - instanceId: "tab-1", - }); - expect(response.statusCode).toBe(409); - expect(response.body).toMatchObject({ - code: "IMAGE_TERMINAL_NOT_CONNECTED", - }); - }); - - it("writes over SFTP in auto mode when a session is connected", async () => { - const { sftp, written, end } = fakeSftp(); - state.sessions = [connectedSession("tab-1", sftp)]; - - const response = await invoke({ - file: { - buffer: pngBuffer, - mimetype: "image/png", - size: pngBuffer.length, - }, - instanceId: "tab-1", - }); - - expect(response.statusCode).toBe(200); - const body = response.body as { - id: string; - filename: string; - shellPath: string; - storage: string; - }; - expect(body.storage).toBe("remote-sftp"); - expect(body.filename).toBe(`${body.id}.png`); - expect(body.shellPath).toBe(`/tmp/termix-images/${body.filename}`); - // Sharp normalized the upload to PNG before the write. - const writtenBytes = written.get(body.shellPath)!; - expect(writtenBytes.subarray(1, 4).toString()).toBe("PNG"); - expect(end).toHaveBeenCalledTimes(1); - }); - - it("maps SFTP failures to 502 without leaking the raw remote error", async () => { - const { sftp, end } = fakeSftp({ - writeError: new Error("Permission denied"), - }); - state.sessions = [connectedSession("tab-1", sftp)]; - - const response = await invoke({ - file: { - buffer: pngBuffer, - mimetype: "image/png", - size: pngBuffer.length, - }, - instanceId: "tab-1", - }); - - expect(response.statusCode).toBe(502); - expect(response.body).toEqual({ - error: "Failed to write image to the remote host", - code: "IMAGE_REMOTE_WRITE_FAILED", - }); - expect(JSON.stringify(response.body)).not.toContain("Permission denied"); - expect(end).toHaveBeenCalledTimes(1); - }); - - it("rejects undecodable image data", async () => { - const { sftp } = fakeSftp(); - state.sessions = [connectedSession("tab-1", sftp)]; - - const response = await invoke({ - file: { - buffer: Buffer.from("definitely not an image"), - mimetype: "image/png", - size: 23, - }, - instanceId: "tab-1", - }); - - expect(response.statusCode).toBe(400); - expect(response.body).toMatchObject({ code: "IMAGE_DECODE_FAILED" }); - }); - - describe("local mapped storage", () => { - let dir: string; - - beforeEach(async () => { - // The settings validator rejects backslashes, so store the temp dir in - // POSIX form. Windows still resolves it for the real file checks. - dir = ( - await fs.mkdtemp(path.join(os.tmpdir(), "termix-route-test-")) - ).replace(/\\/g, "/"); - }); - - afterEach(async () => { - await fs.rm(dir, { recursive: true, force: true }); - }); - - it("stores locally without a terminal session and hides backend paths", async () => { - state.settings["terminal_image_storage_mode"] = "local"; - state.settings["terminal_image_local_dir"] = dir; - state.settings["terminal_image_host_path"] = "/host-view/images"; - - const response = await invoke({ - file: { - buffer: pngBuffer, - mimetype: "image/png", - size: pngBuffer.length, - }, - }); - - expect(response.statusCode).toBe(200); - const body = response.body as { - id: string; - filename: string; - shellPath: string; - storage: string; - }; - expect(body.storage).toBe("local"); - expect(body.shellPath).toBe(`/host-view/images/${body.filename}`); - expect(JSON.stringify(body)).not.toContain(dir); - const stored = await fs.readFile(path.join(dir, body.filename)); - expect(stored.subarray(1, 4).toString()).toBe("PNG"); - }); - - it("answers 507 when the configured image count cap is reached", async () => { - state.settings["terminal_image_storage_mode"] = "local"; - state.settings["terminal_image_local_dir"] = dir; - state.settings["terminal_image_host_path"] = "/host-view/images"; - state.settings["terminal_image_max_count"] = "1"; - await fs.writeFile( - path.join(dir, `${randomUUID()}.png`), - Buffer.alloc(16), - ); - - const response = await invoke({ - file: { - buffer: pngBuffer, - mimetype: "image/png", - size: pngBuffer.length, - }, - }); - - expect(response.statusCode).toBe(507); - expect(response.body).toMatchObject({ - error: "Image storage limit reached", - code: "IMAGE_STORAGE_LIMIT_REACHED", - }); - }); - - it("keeps legacy explicit local mappings on local mode", async () => { - process.env.TERMIX_IMAGE_DIR = dir; - process.env.TERMIX_IMAGE_HOST_PATH = "/host-view/images"; - - const response = await invoke({ - file: { - buffer: pngBuffer, - mimetype: "image/png", - size: pngBuffer.length, - }, - }); - - expect(response.statusCode).toBe(200); - expect(response.body).toMatchObject({ storage: "local" }); - }); - - it("returns 503 when local storage inspection fails", async () => { - const blocked = `${dir}/blocked-file`; - await fs.writeFile(blocked, "not a directory"); - state.settings["terminal_image_storage_mode"] = "local"; - state.settings["terminal_image_local_dir"] = blocked; - state.settings["terminal_image_host_path"] = "/host-view/images"; - - const response = await invoke({ - file: { - buffer: pngBuffer, - mimetype: "image/png", - size: pngBuffer.length, - }, - }); - - expect(response.statusCode).toBe(503); - expect(response.body).toEqual({ - error: "Unable to inspect local image storage", - code: "IMAGE_LOCAL_INSPECTION_FAILED", - }); - expect(JSON.stringify(response.body)).not.toContain(blocked); - }); - it("rejects auto mode when no verified storage capability is available", async () => { - process.env.DATA_DIR = dir; - - const response = await invoke({ - file: { - buffer: pngBuffer, - mimetype: "image/png", - size: pngBuffer.length, - }, - instanceId: "tab-missing", - }); - - expect(response.statusCode).toBe(503); - expect(response.body).toMatchObject({ - code: "IMAGE_STORAGE_UNAVAILABLE", - }); - }); - }); - - describe("diagnostic metadata", () => { - interface UploadLogMeta { - operation?: string; - requestId?: string; - sequence?: number; - source?: string; - clientUploadTimestamp?: string; - serverReceivedAt?: string; - bytes?: number; - } - - function uploadLogEntries(): UploadLogMeta[] { - return vi - .mocked(databaseLogger.info) - .mock.calls.filter( - (call) => - (call[1] as UploadLogMeta | undefined)?.operation === - "terminal_image_upload_received", - ) - .map((call) => call[1] as UploadLogMeta); - } - - beforeEach(() => { - vi.mocked(databaseLogger.info).mockClear(); - }); - - it("logs correlation id, receipt time, and propagated source metadata", async () => { - const { sftp } = fakeSftp(); - state.sessions = [connectedSession("tab-1", sftp)]; - - const response = await invoke({ - file: { - buffer: pngBuffer, - mimetype: "image/png", - size: pngBuffer.length, - }, - instanceId: "tab-1", - metadata: { - source: "clipboard", - clientUploadTimestamp: "2026-08-15T12:00:00.000Z", - }, - }); - - expect(response.statusCode).toBe(200); - const entries = uploadLogEntries(); - expect(entries).toHaveLength(1); - const meta = entries[0]!; - expect(meta.requestId).toMatch( - /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, - ); - expect(typeof meta.sequence).toBe("number"); - expect(meta.source).toBe("clipboard"); - expect(meta.clientUploadTimestamp).toBe("2026-08-15T12:00:00.000Z"); - expect(Number.isNaN(Date.parse(meta.serverReceivedAt ?? ""))).toBe(false); - expect(meta.bytes).toBe(pngBuffer.length); - }); - - it("logs a monotonically increasing upload sequence", async () => { - const { sftp } = fakeSftp(); - state.sessions = [connectedSession("tab-1", sftp)]; - - await invoke({ - file: { - buffer: pngBuffer, - mimetype: "image/png", - size: pngBuffer.length, - }, - instanceId: "tab-1", - metadata: { source: "file" }, - }); - await invoke({ - file: { - buffer: pngBuffer, - mimetype: "image/png", - size: pngBuffer.length, - }, - instanceId: "tab-1", - metadata: { source: "clipboard" }, - }); - - const sequences = uploadLogEntries().map((entry) => entry.sequence!); - expect(sequences).toHaveLength(2); - expect(sequences[1]).toBe(sequences[0]! + 1); - }); - - it("accepts missing metadata and keeps raw paths out of the log", async () => { - const dir = ( - await fs.mkdtemp(path.join(os.tmpdir(), "termix-meta-test-")) - ).replace(/\\/g, "/"); - try { - state.settings["terminal_image_storage_mode"] = "local"; - state.settings["terminal_image_local_dir"] = dir; - state.settings["terminal_image_host_path"] = "/host-view/images"; - - const response = await invoke({ - file: { - buffer: pngBuffer, - mimetype: "image/png", - size: pngBuffer.length, - }, - }); - - expect(response.statusCode).toBe(200); - const entries = uploadLogEntries(); - expect(entries).toHaveLength(1); - expect(entries[0]!.source).toBeUndefined(); - expect(entries[0]!.clientUploadTimestamp).toBeUndefined(); - expect(JSON.stringify(entries[0])).not.toContain(dir); - } finally { - await fs.rm(dir, { recursive: true, force: true }); - } - }); - }); -}); diff --git a/src/backend/tests/database/routes/terminal-image-upload-sharp-unavailable.test.ts b/src/backend/tests/database/routes/terminal-image-upload-sharp-unavailable.test.ts deleted file mode 100644 index d3e4b29a0..000000000 --- a/src/backend/tests/database/routes/terminal-image-upload-sharp-unavailable.test.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; -import type { Request, RequestHandler, Response } from "express"; -import { databaseLogger } from "../../../utils/logger.js"; - -const state = vi.hoisted(() => ({ - userId: "user-1", - settings: {} as Record<string, string>, - sessions: [] as unknown[], -})); - -// A package shipped without the native binary for this architecture. -vi.mock("sharp", () => { - throw new Error( - 'Could not load the "sharp" module using the darwin-x64 runtime', - ); -}); - -vi.mock("../../../utils/logger.js", () => ({ - authLogger: { warn: vi.fn(), error: vi.fn(), info: vi.fn() }, - databaseLogger: { warn: vi.fn(), error: vi.fn(), info: vi.fn() }, -})); - -vi.mock("../../../utils/auth-manager.js", () => ({ - AuthManager: { - getInstance: () => ({ - createAuthMiddleware: - () => (_req: unknown, _res: unknown, next: () => void) => - next(), - createDataAccessMiddleware: - () => (_req: unknown, _res: unknown, next: () => void) => - next(), - }), - }, -})); - -vi.mock("../../../hosts/terminal/session-manager.js", () => ({ - sessionManager: { - getUserSessions: () => state.sessions, - }, -})); - -vi.mock("../../../database/repositories/factory.js", () => ({ - createCurrentSettingsRepository: () => ({ - get: async (key: string) => state.settings[key] ?? null, - }), - createCurrentHostResolutionRepository: () => ({}), - createCurrentCommandHistoryRepository: () => ({}), -})); - -const { default: router } = - await import("../../../database/routes/terminal.js"); - -interface RouteLayer { - route?: { - path: string; - methods: Record<string, boolean>; - stack: Array<{ handle: RequestHandler }>; - }; -} - -const imageUploadLayer = ( - router as unknown as { stack: RouteLayer[] } -).stack.find( - (layer) => layer.route?.path === "/image-upload" && layer.route.methods.post, -); -const imageUploadHandler = - imageUploadLayer!.route!.stack[imageUploadLayer!.route!.stack.length - 1]! - .handle; - -async function invoke() { - const buffer = Buffer.from([0x89, 0x50, 0x4e, 0x47]); - const req = { - userId: state.userId, - body: { instanceId: "tab-1" }, - file: { buffer, mimetype: "image/png", size: buffer.length }, - headers: {}, - } as unknown as Request; - const result = { statusCode: 200, body: null as unknown }; - const res = { - status(code: number) { - result.statusCode = code; - return this; - }, - json(body: unknown) { - result.body = body; - return this; - }, - } as unknown as Response; - await imageUploadHandler(req, res, () => {}); - return result; -} - -beforeEach(() => { - vi.mocked(databaseLogger.error).mockClear(); - state.settings = { terminal_image_storage_mode: "remote-sftp" }; - state.sessions = [{ tabInstanceId: "tab-1", isConnected: true, sshConn: {} }]; -}); - -describe("terminal image upload route when sharp cannot be loaded", () => { - it("imports the router instead of failing at module scope", () => { - expect(imageUploadHandler).toBeTypeOf("function"); - }); - - it("answers 503 and logs the reason rather than throwing", async () => { - const response = await invoke(); - - expect(response.statusCode).toBe(503); - expect(response.body).toMatchObject({ - code: "IMAGE_PROCESSING_UNAVAILABLE", - }); - expect(databaseLogger.error).toHaveBeenCalledWith( - "Image processing unavailable: sharp failed to load", - expect.anything(), - expect.objectContaining({ - operation: "terminal_image_upload_sharp_unavailable", - }), - ); - }); - - it("keeps answering 503 on later uploads without retrying the import", async () => { - await invoke(); - const response = await invoke(); - - expect(response.statusCode).toBe(503); - expect(response.body).toMatchObject({ - code: "IMAGE_PROCESSING_UNAVAILABLE", - }); - }); -}); diff --git a/src/backend/tests/database/routes/touch-input-settings-routes.test.ts b/src/backend/tests/database/routes/touch-input-settings-routes.test.ts deleted file mode 100644 index 83577b9e7..000000000 --- a/src/backend/tests/database/routes/touch-input-settings-routes.test.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; -import type { Request, RequestHandler, Response } from "express"; -import { TOUCH_INPUT_DEFAULTS } from "../../../../types/touch-input-settings.js"; - -const state = vi.hoisted(() => ({ - userId: "admin", - admins: new Set(["admin"]), - stored: null as string | null, -})); - -vi.mock("../../../utils/logger.js", () => ({ - authLogger: { error: vi.fn() }, -})); -vi.mock("../../../database/repositories/factory.js", () => ({ - createCurrentUserRepository: () => ({ - findById: async (id: string) => ({ id, isAdmin: state.admins.has(id) }), - }), - createCurrentSettingsRepository: () => ({ - get: async () => state.stored, - set: async (_key: string, value: string) => { - state.stored = value; - }, - }), -})); - -const { registerTouchInputSettingsRoutes } = - await import("../../../database/routes/touch-input-settings-routes.js"); - -type Registered = { method: string; path: string; handler: RequestHandler }; -const registered: Registered[] = []; -const router = { - get: (path: string, ...handlers: RequestHandler[]) => - registered.push({ method: "get", path, handler: handlers.at(-1)! }), - patch: (path: string, ...handlers: RequestHandler[]) => - registered.push({ method: "patch", path, handler: handlers.at(-1)! }), -} as unknown as import("express").Router; -registerTouchInputSettingsRoutes(router, (_req, _res, next) => next()); - -async function invoke(method: string, body: unknown = {}) { - const handler = registered.find((entry) => entry.method === method)!.handler; - const req = { userId: state.userId, body } as unknown as Request; - const result = { statusCode: 200, body: null as unknown }; - const res = { - status(code: number) { - result.statusCode = code; - return this; - }, - json(bodyValue: unknown) { - result.body = bodyValue; - return this; - }, - } as Response; - await handler(req, res, () => {}); - return result; -} - -beforeEach(() => { - state.userId = "admin"; - state.stored = null; -}); - -describe("touch input settings routes", () => { - it("allows authenticated non-admin users to read normalized defaults", async () => { - state.userId = "user"; - const response = await invoke("get"); - expect(response.statusCode).toBe(200); - expect(response.body).toEqual(TOUCH_INPUT_DEFAULTS); - }); - - it("only allows admins to write", async () => { - state.userId = "user"; - const response = await invoke("patch", { enabled: false }); - expect(response.statusCode).toBe(403); - expect(state.stored).toBeNull(); - }); - - it("rejects out-of-range values and persists normalized updates", async () => { - const invalid = await invoke("patch", { maximumTicksPerFrame: 101 }); - expect(invalid.statusCode).toBe(400); - - const valid = await invoke("patch", { - dragThresholdPx: 9, - momentumEnabled: false, - }); - expect(valid.statusCode).toBe(200); - expect(JSON.parse(state.stored!)).toEqual({ - ...TOUCH_INPUT_DEFAULTS, - dragThresholdPx: 9, - momentumEnabled: false, - }); - }); -}); diff --git a/src/backend/tests/hosts/collab/routes.test.ts b/src/backend/tests/hosts/collab/routes.test.ts index 41fe16932..a0adde1d0 100644 --- a/src/backend/tests/hosts/collab/routes.test.ts +++ b/src/backend/tests/hosts/collab/routes.test.ts @@ -101,8 +101,8 @@ vi.mock("../../../hosts/collab/runtime-store.js", () => ({ }, }, })); -vi.mock("../../../hosts/terminal/session-manager.js", () => ({ - sessionManager: { +vi.mock("../../../hosts/live-terminal-sessions.js", () => ({ + liveTerminalSessions: { setRoomShareControl: (...args: unknown[]) => { state.control.push(args); }, diff --git a/src/backend/tests/hosts/session-sharing/routes.test.ts b/src/backend/tests/hosts/session-sharing/routes.test.ts index e98d7c5e2..086769666 100644 --- a/src/backend/tests/hosts/session-sharing/routes.test.ts +++ b/src/backend/tests/hosts/session-sharing/routes.test.ts @@ -52,8 +52,8 @@ vi.mock("../../../utils/permission-manager.js", () => ({ }, })); -vi.mock("../../../hosts/terminal/session-manager.js", () => ({ - sessionManager: { +vi.mock("../../../hosts/live-terminal-sessions.js", () => ({ + liveTerminalSessions: { getSession: (sessionId: string) => { const session = state.sshSessions.get(sessionId); if (!session) return null; diff --git a/src/backend/tests/hosts/terminal-agent-auth.test.ts b/src/backend/tests/hosts/terminal-agent-auth.test.ts index 61b316d16..532fa44e3 100644 --- a/src/backend/tests/hosts/terminal-agent-auth.test.ts +++ b/src/backend/tests/hosts/terminal-agent-auth.test.ts @@ -9,57 +9,10 @@ vi.mock("fs/promises", () => ({ })); import { - MemoryAgent, FilteredAgent, resolveAgentSocket, } from "../../hosts/terminal-auth-helpers.js"; -describe("MemoryAgent", () => { - it("serves identities and signatures over the agent protocol", async () => { - const { privateKey } = generateKeyPairSync("rsa", { modulusLength: 2048 }); - const parsed = ssh2Pkg.utils.parseKey( - privateKey.export({ type: "pkcs1", format: "pem" }), - ); - expect(parsed).not.toBeInstanceOf(Error); - - const agent = new MemoryAgent(parsed as ParsedKey); - const stream = await new Promise<NodeJS.ReadWriteStream>( - (resolve, reject) => { - agent.getStream((error, result) => { - if (error || !result) - reject(error ?? new Error("Missing agent stream")); - else resolve(result); - }); - }, - ); - const client = new ssh2Pkg.AgentProtocol(true); - client.pipe(stream).pipe(client); - - const identities = await new Promise<ParsedKey[]>((resolve, reject) => { - client.getIdentities((error, keys) => { - if (error || !keys) reject(error ?? new Error("Missing identities")); - else resolve(keys); - }); - }); - expect(identities).toHaveLength(1); - expect(identities[0].getPublicSSH()).toEqual( - (parsed as ParsedKey).getPublicSSH(), - ); - - const data = Buffer.from("forwarded-agent-test"); - const signature = await new Promise<Buffer>((resolve, reject) => { - client.sign(identities[0], data, (error, result) => { - if (error || !result) reject(error ?? new Error("Missing signature")); - else resolve(result); - }); - }); - expect((parsed as ParsedKey).verify(data, signature)).toBe(true); - - client.destroy(); - stream.destroy(); - }); -}); - describe("resolveAgentSocket", () => { const originalEnv = process.env.SSH_AUTH_SOCK; const originalPlatform = process.platform; @@ -228,58 +181,3 @@ describe("FilteredAgent", () => { expect(identities).toHaveLength(0); }); }); - -describe("MemoryAgent unsupported extension framing", () => { - it.each([false, true])( - "can enumerate keys after rejecting session-bind (fragmented=%s)", - async (fragmented) => { - const { privateKey } = generateKeyPairSync("rsa", { - modulusLength: 2048, - }); - const key = ssh2Pkg.utils.parseKey( - privateKey.export({ type: "pkcs1", format: "pem" }), - ) as ParsedKey; - const agent = new MemoryAgent(key); - const stream = await new Promise<NodeJS.ReadWriteStream>( - (resolve, reject) => { - agent.getStream((error, result) => - error || !result ? reject(error) : resolve(result), - ); - }, - ); - const replies: Buffer[] = []; - let buffered = Buffer.alloc(0); - stream.on("data", (data: Buffer) => { - buffered = Buffer.concat([buffered, data]); - while ( - buffered.length >= 4 && - buffered.length >= buffered.readUInt32BE(0) + 4 - ) { - const end = buffered.readUInt32BE(0) + 4; - replies.push(buffered.subarray(4, end)); - buffered = buffered.subarray(end); - } - }); - const extension = Buffer.from("session-bind@openssh.com"); - const request = Buffer.alloc(4 + 1 + 4 + extension.length); - request.writeUInt32BE(request.length - 4, 0); - request[4] = 27; - request.writeUInt32BE(extension.length, 5); - extension.copy(request, 9); - const identities = Buffer.from([0, 0, 0, 1, 11]); - try { - const requests = Buffer.concat([request, identities]); - if (fragmented) { - stream.write(requests.subarray(0, 7)); - stream.write(requests.subarray(7)); - } else stream.write(requests); - await vi.waitFor(() => expect(replies).toHaveLength(2)); - expect(replies[0][0]).toBe(5); // Unsupported extension remains refused. - expect(replies[1][0]).toBe(12); // The following identities request succeeds. - expect(replies[1].readUInt32BE(1)).toBe(1); - } finally { - stream.destroy(); - } - }, - ); -}); diff --git a/src/backend/tests/hosts/terminal/server-lifecycle.test.ts b/src/backend/tests/hosts/terminal/server-lifecycle.test.ts deleted file mode 100644 index dcfb29ce5..000000000 --- a/src/backend/tests/hosts/terminal/server-lifecycle.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -/** - * The terminal WS server's lifecycle, which the ssh-terminal plugin drives. - * - * There is no terminal port any more: core serves the socket at - * /plugin-ws/ssh-terminal/terminal and the plugin hands each upgrade to the - * WebSocketServer this module owns. So the thing worth asserting changed with - * it. It used to be "the port stops listening"; it is now "the module binds - * nothing on import, and disabling the plugin closes the live sessions", which - * is what disabling a transport owner has to mean when core owns the socket. - * - * The terminal module is heavy (it pulls in the whole SSH stack), so this - * exercises only the lifecycle surface. - */ - -import { describe, expect, it, vi } from "vitest"; - -vi.mock("../../../database/repositories/factory.js", () => ({ - createCurrentHostResolutionRepository: () => ({}), - createCurrentCollabRoomRepository: () => ({}), - createCurrentSessionShareRepository: () => ({}), - createCurrentSettingsRepository: () => ({}), - createCurrentSessionRecordingRepository: () => ({}), -})); - -describe("terminal server lifecycle", () => { - it("binds no port of its own", async () => { - const terminal = await import("../../../hosts/terminal/index.js"); - - // The old module exported a port and a listen() call. Both are gone, and - // their absence is the contract: a plugin that binds its own port is - // exactly what A4 removed. - expect("TERMINAL_WS_PORT" in terminal).toBe(false); - expect("startTerminalServer" in terminal).toBe(false); - }); - - it("exposes an upgrade handler for core to route to", async () => { - const { handleTerminalUpgrade } = - await import("../../../hosts/terminal/index.js"); - - expect(typeof handleTerminalUpgrade).toBe("function"); - }); - - it("is idempotent on stop, so a double deactivate is safe", async () => { - const { stopTerminalServer } = - await import("../../../hosts/terminal/index.js"); - - await expect(stopTerminalServer()).resolves.toBeUndefined(); - await expect(stopTerminalServer()).resolves.toBeUndefined(); - }); -}); diff --git a/src/backend/tests/plugins/ctx-audit.test.ts b/src/backend/tests/plugins/ctx-audit.test.ts new file mode 100644 index 000000000..d2d4634e8 --- /dev/null +++ b/src/backend/tests/plugins/ctx-audit.test.ts @@ -0,0 +1,83 @@ +/** + * ctx.audit.record: a plugin writes an audit line under its own action name, + * and the user it names is always the runtime's actor, never one the plugin + * picked. + */ + +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const auditEntries: Array<Record<string, unknown>> = []; + +vi.mock("../../database/repositories/factory.js", () => ({ + createCurrentPluginPermissionGrantRepository: () => ({ + listByPlugin: async () => [], + }), +})); + +vi.mock("../../utils/audit-logger.js", () => ({ + logAudit: async (entry: Record<string, unknown>) => { + auditEntries.push(entry); + }, +})); + +import { createPluginContext, createPluginHandle } from "../../plugins/ctx.js"; +import { runAsActor } from "../../plugins/actor.js"; +import type { PluginManifest } from "@termix/plugin-sdk/manifest"; + +function contextFor(pluginId: string) { + const manifest = { + id: pluginId, + name: "Audit Fixture", + version: "1.0.0", + description: "", + author: { name: "test" }, + license: "MIT", + category: "Productivity", + engine: { termix: ">=2.9.0", api: "1" }, + capabilities: [], + } as PluginManifest; + const handle = createPluginHandle(pluginId, { activate: () => {} }); + return createPluginContext(manifest, handle); +} + +beforeEach(() => { + auditEntries.length = 0; +}); + +describe("ctx.audit.record", () => { + it("writes the plugin's action for the acting user", async () => { + const ctx = contextFor("audit-fixture"); + await runAsActor("user-3", "request", () => + ctx.audit.record({ + action: "ssh_connect", + resourceType: "host", + resourceId: "12", + resourceName: "root@10.0.0.1:22", + success: true, + }), + ); + expect(auditEntries).toEqual([ + expect.objectContaining({ + userId: "user-3", + action: "ssh_connect", + resourceType: "host", + resourceId: "12", + resourceName: "root@10.0.0.1:22", + details: "via plugin audit-fixture", + success: true, + }), + ]); + }); + + it("falls back to the plugin as the resource and system as the user", async () => { + const ctx = contextFor("audit-fixture"); + await ctx.audit.record({ action: "cleanup", success: false }); + expect(auditEntries[0]).toMatchObject({ + userId: "system", + resourceType: "plugin", + resourceId: "audit-fixture", + resourceName: "Audit Fixture", + success: false, + }); + }); +}); diff --git a/src/backend/tests/plugins/ctx-hosts.test.ts b/src/backend/tests/plugins/ctx-hosts.test.ts index 0239648a6..416328fbc 100644 --- a/src/backend/tests/plugins/ctx-hosts.test.ts +++ b/src/backend/tests/plugins/ctx-hosts.test.ts @@ -18,6 +18,9 @@ const h = vi.hoisted(() => ({ updates: [] as Array<Record<string, unknown>>, updateResult: null as Record<string, unknown> | null, users: [] as Array<{ id: string; username: string }>, + activities: [] as Array<Record<string, unknown>>, + sessions: [] as number[], + released: [] as number[], roles: [] as Array<{ id: number; name: string; @@ -90,6 +93,23 @@ vi.mock("../../database/repositories/factory.js", () => ({ listDecryptedByUserId: async () => h.ownedHosts, }), })); +vi.mock("../../services/recent-activity.js", () => ({ + recordRecentActivity: async ( + userId: string, + entry: Record<string, unknown>, + ) => { + h.activities.push({ userId, ...entry }); + return { status: "logged", id: 1 }; + }, +})); +vi.mock("../../hosts/host-session-status.js", () => ({ + hostSessionStatus: { + register: (hostId: number) => { + h.sessions.push(hostId); + return () => h.released.push(hostId); + }, + }, +})); vi.mock("../../utils/shared-host-secrets-manager.js", () => ({ SharedHostSecretsManager: { getInstance: () => ({ @@ -347,3 +367,40 @@ describe("ctx.hosts", () => { expect(await hosts.listOwned()).toMatchObject([{ id: 1, name: "own" }]); }); }); + +describe("ctx.hosts session tracking and activity", () => { + it("counts a live session through core and releases it", () => { + h.sessions = []; + h.released = []; + const hosts = createPluginHosts({ + manifest: manifest(["hosts:read"]), + audit: vi.fn(async () => {}), + }); + const release = hosts.trackSession(4); + expect(h.sessions).toEqual([4]); + release(); + expect(h.released).toEqual([4]); + }); + + it("refuses to track a session without hosts:read declared", () => { + const hosts = createPluginHosts({ + manifest: manifest([]), + audit: vi.fn(async () => {}), + }); + expect(() => hosts.trackSession(4)).toThrow(PluginCapabilityError); + }); + + it("records activity for the acting user only", async () => { + h.activities = []; + h.granted = new Set(["hosts:read"]); + h.actor = "user-7"; + const hosts = createPluginHosts({ + manifest: manifest(["hosts:read"]), + audit: vi.fn(async () => {}), + }); + await hosts.recordActivity(4, "terminal", "web-01"); + expect(h.activities).toEqual([ + { userId: "user-7", type: "terminal", hostId: 4, hostName: "web-01" }, + ]); + }); +}); diff --git a/src/backend/tests/plugins/ctx-ssh-interactive.test.ts b/src/backend/tests/plugins/ctx-ssh-interactive.test.ts new file mode 100644 index 000000000..f5e5f6c46 --- /dev/null +++ b/src/backend/tests/plugins/ctx-ssh-interactive.test.ts @@ -0,0 +1,263 @@ +/** + * The ctx.ssh members an interactive transport (the terminal) needs: host + * resolution with secrets, prepare with its provider hooks bound, and the + * browser sign-in start and cancel that used to be reached by import. + */ + +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const h = vi.hoisted(() => ({ + granted: new Set<string>(), + actor: "user-1" as string | undefined, + resolved: [] as Array<{ by: string; ref: unknown; userId: string }>, + built: [] as Array<Record<string, unknown>>, +})); + +vi.mock("../../utils/logger.js", () => { + const log = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }; + return { pluginLogger: log, sshLogger: log, logger: log, authLogger: log }; +}); +vi.mock("../../plugins/permissions.js", async () => { + const { PluginCapabilityError } = await import("@termix/plugin-sdk/backend"); + return { + assertCapability: async ( + pluginId: string, + capability: string, + declared: readonly string[], + ) => { + if (!declared.includes(capability) || !h.granted.has(capability)) { + throw new PluginCapabilityError(pluginId, capability); + } + }, + }; +}); +vi.mock("../../plugins/actor.js", () => ({ getActor: () => h.actor })); +vi.mock("../../hosts/connect/core-providers.js", () => ({ + ensureCoreSshAuthProviders: () => {}, +})); +vi.mock("../../hosts/host-resolver.js", () => ({ + resolveHostById: async (id: number, userId: string) => { + h.resolved.push({ by: "id", ref: id, userId }); + return id === 404 ? null : { id, ip: "10.0.0.1", password: "pw" }; + }, + resolveHostBySyncId: async (syncId: string, userId: string) => { + h.resolved.push({ by: "syncId", ref: syncId, userId }); + return { id: 9, ip: "10.0.0.9" }; + }, +})); +vi.mock("../../hosts/connect/build-connect-config.js", () => ({ + buildConnectConfig: async ( + _host: unknown, + options: Record<string, unknown>, + ) => { + h.built.push(options); + return { + config: { host: "10.0.0.1" }, + outcome: { status: "ready" }, + env: { interactive: options.interactive }, + provider: { + type: "fixture-auth", + onBanner: (banner: string, _host: unknown, env: unknown) => ({ + action: "hold", + timeoutMs: 1, + message: banner, + details: { env }, + }), + onAuthFailed: () => ({ status: "retry", patch: {}, message: "again" }), + }, + }; + }, +})); +vi.mock("../../database/repositories/factory.js", () => ({ + createCurrentHostResolutionRepository: () => ({ + findHostById: async (hostId: number) => + hostId === 1 + ? { + id: 1, + name: "box", + ip: "10.0.0.1", + username: "root", + authType: "fixture-auth", + } + : null, + }), +})); + +const { createPluginSsh } = await import("../../plugins/ctx-ssh-auth.js"); +const { DisposableBag } = await import("../../plugins/disposables.js"); +const { registerSshAuthProvider, resetSshAuthRegistryForTests } = + await import("../../hosts/connect/auth-provider-registry.js"); +const { PluginCapabilityError, PluginSshInteractionError } = + await import("@termix/plugin-sdk/backend"); + +const ALL = ["ssh:connect", "credentials:use"]; + +function sshWith(capabilities = ALL) { + const audit = vi.fn(async () => {}); + const ssh = createPluginSsh({ + manifest: { + id: "fixture", + name: "Fixture", + version: "1.0.0", + capabilities, + } as never, + bag: new DisposableBag("fixture"), + audit, + }); + return { ssh, audit }; +} + +beforeEach(() => { + h.granted = new Set(ALL); + h.actor = "user-1"; + h.resolved = []; + h.built = []; + resetSshAuthRegistryForTests(); +}); + +describe("ctx.ssh.resolveHost", () => { + it("resolves by sync id first, as the acting user, and audits", async () => { + const { ssh, audit } = sshWith(); + expect(await ssh.resolveHost(3, { syncId: "abc" })).toMatchObject({ + id: 9, + }); + expect(await ssh.resolveHost(3)).toMatchObject({ id: 3, password: "pw" }); + expect(h.resolved).toEqual([ + { by: "syncId", ref: "abc", userId: "user-1" }, + { by: "id", ref: 3, userId: "user-1" }, + ]); + expect(audit).toHaveBeenCalledWith("ssh_resolve_host", "host 3", { + success: true, + }); + }); + + it("answers null for a host this user cannot reach", async () => { + const { ssh } = sshWith(); + expect(await ssh.resolveHost(404)).toBeNull(); + }); + + it("refuses without credentials:use", async () => { + h.granted = new Set(["ssh:connect"]); + const { ssh } = sshWith(["ssh:connect"]); + await expect(ssh.resolveHost(3)).rejects.toBeInstanceOf( + PluginCapabilityError, + ); + expect(h.resolved).toEqual([]); + }); +}); + +describe("ctx.ssh.prepare", () => { + it("passes the terminal purpose, interactivity and host key socket through", async () => { + const { ssh } = sshWith(); + const socket = { send: vi.fn() }; + await ssh.prepare( + { id: 1, ip: "10.0.0.1", port: 22, username: "root" }, + { + purpose: "terminal", + client: {}, + interactive: true, + hostKeySocket: socket, + }, + ); + expect(h.built[0]).toMatchObject({ + purpose: "terminal", + interactive: true, + hostKeySocket: socket, + userId: "user-1", + }); + }); + + it("hands back the provider's hooks bound to this host", async () => { + const { ssh } = sshWith(); + const prepared = await ssh.prepare( + { id: 1, ip: "10.0.0.1", port: 22, username: "root" }, + { client: {}, interactive: true }, + ); + expect(prepared.authType).toBe("fixture-auth"); + expect(prepared.onBanner?.("waiting")).toMatchObject({ + action: "hold", + message: "waiting", + details: { env: { interactive: true } }, + }); + expect( + prepared.onAuthFailed?.({ + error: new Error("x"), + retries: 0, + canRetry: true, + methodNotAvailable: false, + }), + ).toMatchObject({ status: "retry" }); + }); +}); + +describe("ctx.ssh browser sign-in", () => { + it("starts the interaction on the host's own provider", async () => { + const started = vi.fn(async () => {}); + registerSshAuthProvider({ + type: "fixture-auth", + pluginId: "core", + labelKey: "k", + interaction: "fixture", + startInteraction: started, + prepare: async () => ({ status: "ready" }), + }); + const { ssh } = sshWith(); + await ssh.startInteraction("fixture", { + hostId: 1, + socket: "ws", + requestOrigin: "https://termix.example", + }); + expect(started).toHaveBeenCalledWith( + expect.objectContaining({ + userId: "user-1", + hostId: 1, + socket: "ws", + requestOrigin: "https://termix.example", + host: { name: "box", ip: "10.0.0.1", username: "root" }, + }), + ); + }); + + it("says why when nothing can start it", async () => { + const { ssh } = sshWith(); + await expect( + ssh.startInteraction("fixture", { + hostId: 1, + socket: null, + requestOrigin: "", + }), + ).rejects.toBeInstanceOf(PluginSshInteractionError); + await expect( + ssh.startInteraction("fixture", { + hostId: 2, + socket: null, + requestOrigin: "", + }), + ).rejects.toThrow("Host not found"); + }); + + it("cancels on every provider that owns the interaction", async () => { + const first = vi.fn(); + const second = vi.fn(); + const other = vi.fn(); + for (const [type, interaction, cancel] of [ + ["a", "fixture", first], + ["b", "fixture", second], + ["c", "elsewhere", other], + ] as const) { + registerSshAuthProvider({ + type, + pluginId: "core", + labelKey: "k", + interaction, + cancelInteraction: cancel, + prepare: async () => ({ status: "ready" }), + }); + } + const { ssh } = sshWith(); + await ssh.cancelInteraction("fixture", { requestId: "r1" }); + expect(first).toHaveBeenCalledWith({ userId: "user-1", requestId: "r1" }); + expect(second).toHaveBeenCalledOnce(); + expect(other).not.toHaveBeenCalled(); + }); +}); diff --git a/src/backend/tests/plugins/service-registry.test.ts b/src/backend/tests/plugins/service-registry.test.ts index cf5d9fbd2..1d6c14f5e 100644 --- a/src/backend/tests/plugins/service-registry.test.ts +++ b/src/backend/tests/plugins/service-registry.test.ts @@ -21,6 +21,7 @@ const { clearServiceRegistry, createServiceHandle, getRegistration, + getServiceImplementation, listServices, provideService, resolveRequirements, @@ -85,6 +86,19 @@ describe("service registry", () => { expect(revokeService("nothing.here")).toBe(false); }); + it("hands core the implementation of a compatible version, or nothing", () => { + expect( + getServiceImplementation("testplugin.greet", "^1.0.0"), + ).toBeUndefined(); + const registration = register({ version: "1.4.0" }); + expect(getServiceImplementation("testplugin.greet", "^1.0.0")).toBe( + registration.implementation, + ); + expect( + getServiceImplementation("testplugin.greet", "^2.0.0"), + ).toBeUndefined(); + }); + describe("resolution", () => { it("is satisfied when the version is in range", () => { register({ version: "1.2.0" }); diff --git a/src/backend/tests/plugins/ws.test.ts b/src/backend/tests/plugins/ws.test.ts index 7f5c45ec5..9f6520cd5 100644 --- a/src/backend/tests/plugins/ws.test.ts +++ b/src/backend/tests/plugins/ws.test.ts @@ -37,6 +37,13 @@ vi.mock("../../utils/auth-manager.js", () => ({ }, })); +vi.mock("../../utils/data-crypto.js", () => ({ + DataCrypto: { + getUserDataKey: (userId: string) => + userId === state.userId ? Buffer.from("key") : null, + }, +})); + vi.mock("../../plugins/permissions.js", () => ({ hasCapability: async ( _pluginId: string, @@ -190,6 +197,54 @@ describe("ctx.ws authentication", () => { }); }); +describe("ctx.ws optional auth", () => { + function register(seen: Array<Record<string, unknown>>) { + ws.registerPluginWsRoute( + "sample-plugin", + "/mixed", + (connection) => { + seen.push({ + userId: connection.userId, + unlocked: connection.isDataUnlocked(), + clientIp: connection.clientIp, + requestOrigin: connection.requestOrigin, + }); + (connection.socket as WebSocket).send("hi"); + }, + DECLARED, + { public: true, optionalAuth: true }, + ); + } + + it("names the user when a valid token came with the upgrade", async () => { + const seen: Array<Record<string, unknown>> = []; + register(seen); + const base = await startServer(); + const result = await connect( + `${base}/plugin-ws/sample-plugin/mixed`, + authProtocols(state.validToken), + ); + expect(result.outcome).toBe("open"); + expect(seen[0]).toMatchObject({ userId: state.userId, unlocked: true }); + expect(typeof seen[0].clientIp).toBe("string"); + expect(String(seen[0].requestOrigin)).toMatch(/^http/); + if (result.outcome === "open") result.socket.close(); + }); + + it("still serves a guest, with no user and nothing unlocked", async () => { + const seen: Array<Record<string, unknown>> = []; + register(seen); + const base = await startServer(); + const result = await connect( + `${base}/plugin-ws/sample-plugin/mixed`, + authProtocols("nope"), + ); + expect(result.outcome).toBe("open"); + expect(seen[0]).toMatchObject({ userId: "", unlocked: false }); + if (result.outcome === "open") result.socket.close(); + }); +}); + describe("ctx.ws capability", () => { it("refuses the upgrade when network:serve is not granted", async () => { ws.registerPluginWsRoute("sample-plugin", "/socket", () => {}, DECLARED); diff --git a/src/backend/tests/utils/login-event-dispatch.test.ts b/src/backend/tests/utils/login-event-dispatch.test.ts deleted file mode 100644 index 91f54e6fc..000000000 --- a/src/backend/tests/utils/login-event-dispatch.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -/** - * dispatchLoginEvent used to POST to the metrics service on localhost:30005 - * with an internal auth token. Host metrics is a plugin now and has no port, - * so it publishes on the plugin event bus and the plugin subscribes. - * - * What is asserted changed with it: the payload a subscriber receives, and - * that a throwing subscriber cannot fail the login that caused the event. - */ - -import { afterEach, describe, expect, it, vi } from "vitest"; -import { dispatchLoginEvent } from "../../utils/login-event-dispatch.js"; -import { pluginEvents, TOPICS } from "../../plugins/events.js"; -import { sshLogger } from "../../utils/logger.js"; - -describe("dispatchLoginEvent", () => { - afterEach(() => { - vi.restoreAllMocks(); - }); - - it("publishes the login details a subscriber needs", async () => { - const received: unknown[] = []; - const unsubscribe = pluginEvents.on(TOPICS.hostLogin, (payload) => { - received.push(payload); - }); - - try { - await dispatchLoginEvent(42, "user-1", "root", "10.0.0.5"); - } finally { - unsubscribe(); - } - - expect(received).toEqual([ - { hostId: 42, userId: "user-1", sshUser: "root", fromIp: "10.0.0.5" }, - ]); - }); - - it("does not log a warning on the happy path", async () => { - const warn = vi.spyOn(sshLogger, "warn").mockImplementation(() => {}); - - await dispatchLoginEvent(7, "user-1", "root", "192.0.2.1"); - - expect(warn).not.toHaveBeenCalled(); - }); - - it("never sends the event over the network", async () => { - const fetchSpy = vi.spyOn(globalThis, "fetch"); - - await dispatchLoginEvent(7, "user-1", "root", "192.0.2.1"); - - // The whole point of the move: no service-to-service HTTP call, and no - // internal shared secret to carry on it. - expect(fetchSpy).not.toHaveBeenCalled(); - }); - - it("does not propagate a subscriber's throw to the caller", async () => { - const unsubscribe = pluginEvents.on(TOPICS.hostLogin, () => { - throw new Error("subscriber exploded"); - }); - - try { - await expect( - dispatchLoginEvent(1, "user-1", "root", "127.0.0.1"), - ).resolves.toBeUndefined(); - } finally { - unsubscribe(); - } - }); -}); diff --git a/src/backend/tests/utils/ssh-terminal-settings-migration.test.ts b/src/backend/tests/utils/ssh-terminal-settings-migration.test.ts new file mode 100644 index 000000000..c7d62ece8 --- /dev/null +++ b/src/backend/tests/utils/ssh-terminal-settings-migration.test.ts @@ -0,0 +1,217 @@ +/** + * The SSH terminal settings migration. + * + * An upgrade must be lossless: an install's session timeout, command history + * switch, touch tuning and image storage settings, and every host that turned + * a terminal switch off, must come through into the plugin's own settings. + * Running it twice must not duplicate or clobber what it moved. + */ + +import { beforeEach, describe, expect, it, vi } from "vitest"; + +interface Row { + pluginId: string; + scope: string; + scopeId: string | null; + key: string; + value: string | null; + encrypted: boolean; +} + +interface HostRow { + id: number; + enable_terminal: number | null; + enable_terminal_toolbar: number | null; + enable_command_history: number | null; +} + +const coreSettings = new Map<string, string>(); +const pluginRows: Row[] = []; +const hostRows: HostRow[] = []; +let pluginInstalled = true; + +const find = ( + pluginId: string, + scope: string, + scopeId: string | null, + key: string, +) => + pluginRows.find( + (row) => + row.pluginId === pluginId && + row.scope === scope && + row.scopeId === scopeId && + row.key === key, + ); + +vi.mock("../../database/repositories/factory.js", () => ({ + createCurrentPluginRepository: () => ({ + findById: async (id: string) => + pluginInstalled && id === "ssh-terminal" ? { id } : null, + }), + createCurrentPluginSettingsRepository: () => ({ + 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, + encrypted = false, + ) => { + const existing = find(pluginId, scope, scopeId, key); + if (existing) { + existing.value = value; + existing.encrypted = encrypted; + return; + } + pluginRows.push({ pluginId, scope, scopeId, key, value, encrypted }); + }, + }), + createCurrentSettingsRepository: () => ({ + get: async (key: string) => coreSettings.get(key) ?? null, + }), +})); + +vi.mock("../../database/db/index.js", () => ({ + getDb: () => ({ + all: async () => + hostRows.filter( + (row) => + row.enable_terminal === 0 || + row.enable_terminal_toolbar === 0 || + row.enable_command_history === 0, + ), + }), +})); + +vi.mock("../../utils/system-secret-crypto.js", () => ({ + encryptSystemSecret: async (plaintext: string) => + plaintext.startsWith("sysenc:") ? plaintext : `sysenc:${plaintext}`, +})); + +const { runSshTerminalSettingsMigration } = + await import("../../utils/crypto-migration/ssh-terminal-settings-migration.js"); + +function admin(key: string): unknown { + const row = find("ssh-terminal", "admin", null, key); + return row?.value == null ? undefined : JSON.parse(row.value); +} + +function host(id: number, key: string): unknown { + const row = find("ssh-terminal", "host", String(id), key); + return row?.value == null ? undefined : JSON.parse(row.value); +} + +beforeEach(() => { + coreSettings.clear(); + pluginRows.length = 0; + hostRows.length = 0; + pluginInstalled = true; +}); + +describe("runSshTerminalSettingsMigration", () => { + it("moves the admin settings with their types, the local dir as a secret", async () => { + coreSettings.set("terminal_session_timeout_minutes", "120"); + coreSettings.set("terminal_session_persistence_enabled", "false"); + coreSettings.set("command_history_enabled", "false"); + coreSettings.set( + "touch_input_settings", + JSON.stringify({ enabled: false, dragThresholdPx: 12 }), + ); + coreSettings.set("terminal_image_storage_mode", "local"); + coreSettings.set("terminal_image_local_dir", "/data/images"); + coreSettings.set("terminal_image_max_count", "50"); + + const result = await runSshTerminalSettingsMigration(); + + expect(result.moved).toHaveLength(7); + expect(admin("sessionTimeoutMinutes")).toBe(120); + expect(admin("sessionPersistence")).toBe(false); + expect(admin("commandHistoryEnabled")).toBe(false); + expect(admin("touchInput")).toEqual({ + enabled: false, + dragThresholdPx: 12, + }); + expect(admin("imageStorageMode")).toBe("local"); + expect(admin("imageMaxCount")).toBe(50); + const localDir = find("ssh-terminal", "admin", null, "imageLocalDir"); + expect(localDir?.encrypted).toBe(true); + expect(JSON.parse(localDir!.value!)).toBe("sysenc:/data/images"); + }); + + it("copies each host that switched something off, keeping the rest on", async () => { + hostRows.push( + { + id: 1, + enable_terminal: 1, + enable_terminal_toolbar: 0, + enable_command_history: 1, + }, + { + id: 2, + enable_terminal: 0, + enable_terminal_toolbar: 1, + enable_command_history: 0, + }, + { + id: 3, + enable_terminal: 1, + enable_terminal_toolbar: 1, + enable_command_history: 1, + }, + ); + + const result = await runSshTerminalSettingsMigration(); + + expect(result.hostsMoved).toBe(2); + expect(host(1, "enableTerminal")).toBe(true); + expect(host(1, "enableTerminalToolbar")).toBe(false); + expect(host(1, "enableCommandHistory")).toBe(true); + expect(host(2, "enableTerminal")).toBe(false); + expect(host(2, "enableCommandHistory")).toBe(false); + // All on is the plugin's default, so it needs no row. + expect(host(3, "enableTerminal")).toBeUndefined(); + }); + + it("changes nothing on a second run, even after the admin changed a value", async () => { + coreSettings.set("terminal_session_timeout_minutes", "120"); + hostRows.push({ + id: 1, + enable_terminal: 0, + enable_terminal_toolbar: 1, + enable_command_history: 1, + }); + await runSshTerminalSettingsMigration(); + + const changed = find( + "ssh-terminal", + "admin", + null, + "sessionTimeoutMinutes", + ); + changed!.value = JSON.stringify(15); + const rowsBefore = pluginRows.length; + + const second = await runSshTerminalSettingsMigration(); + + expect(second.moved).toEqual([]); + expect(second.hostsMoved).toBe(0); + expect(second.hostsSkipped).toBe(1); + expect(pluginRows).toHaveLength(rowsBefore); + expect(admin("sessionTimeoutMinutes")).toBe(15); + }); + + it("does nothing until the plugin row exists", async () => { + pluginInstalled = false; + coreSettings.set("terminal_session_timeout_minutes", "120"); + const result = await runSshTerminalSettingsMigration(); + expect(result.moved).toEqual([]); + expect(pluginRows).toEqual([]); + }); +}); diff --git a/src/backend/utils/crypto-migration/ssh-terminal-settings-migration.ts b/src/backend/utils/crypto-migration/ssh-terminal-settings-migration.ts new file mode 100644 index 000000000..24db113a4 --- /dev/null +++ b/src/backend/utils/crypto-migration/ssh-terminal-settings-migration.ts @@ -0,0 +1,235 @@ +/** + * Moves the terminal's settings out of core and into the ssh-terminal + * plugin's own settings. + * + * Admin keys from the core `settings` table: session persistence, command + * history, touch input tuning and terminal image storage. The image storage + * local directory was never sent to a browser, so it lands as a secret field, + * which keeps it that way. The legacy rows are left in place for a release; + * nothing reads them any more. + * + * Host columns from ssh_data: enable_terminal, enable_terminal_toolbar and + * enable_command_history. All three default to on in the plugin, so only + * hosts that turned one off need a row. + * + * Idempotent: an admin key already set in the plugin, or a host that already + * has an enableTerminal row, is skipped, so this is safe on every boot. + */ + +import { sql } from "drizzle-orm"; +import { databaseLogger } from "../logger.js"; +import { getDb } from "../../database/db/index.js"; +import { + createCurrentPluginRepository, + createCurrentPluginSettingsRepository, + createCurrentSettingsRepository, +} from "../../database/repositories/factory.js"; +import { encryptSystemSecret } from "../system-secret-crypto.js"; + +const PLUGIN_ID = "ssh-terminal"; + +type Parse = (raw: string) => unknown; + +const asNumber: Parse = (raw) => { + const value = Number(raw); + return Number.isFinite(value) ? value : undefined; +}; +const asBoolean: Parse = (raw) => + raw === "true" || raw === "1" + ? true + : raw === "false" || raw === "0" + ? false + : undefined; +const asString: Parse = (raw) => raw; +const asJson: Parse = (raw) => { + try { + return JSON.parse(raw); + } catch { + return undefined; + } +}; + +/** Legacy core key -> the admin field the plugin declares. */ +const ADMIN_MOVES: { + legacyKey: string; + field: string; + parse: Parse; + secret?: boolean; +}[] = [ + { + legacyKey: "terminal_session_timeout_minutes", + field: "sessionTimeoutMinutes", + parse: asNumber, + }, + { + legacyKey: "terminal_session_persistence_enabled", + field: "sessionPersistence", + parse: asBoolean, + }, + { + legacyKey: "command_history_enabled", + field: "commandHistoryEnabled", + parse: asBoolean, + }, + { legacyKey: "touch_input_settings", field: "touchInput", parse: asJson }, + { + legacyKey: "terminal_image_storage_mode", + field: "imageStorageMode", + parse: asString, + }, + { + legacyKey: "terminal_image_local_dir", + field: "imageLocalDir", + parse: asString, + secret: true, + }, + { + legacyKey: "terminal_image_host_path", + field: "imageHostPath", + parse: asString, + }, + { legacyKey: "terminal_image_ttl_ms", field: "imageTtlMs", parse: asNumber }, + { + legacyKey: "terminal_image_max_count", + field: "imageMaxCount", + parse: asNumber, + }, + { + legacyKey: "terminal_image_max_storage_bytes", + field: "imageMaxBytes", + parse: asNumber, + }, +]; + +interface LegacyTerminalHostRow { + id: number; + enable_terminal: number | boolean | null; + enable_terminal_toolbar: number | boolean | null; + enable_command_history: number | boolean | null; +} + +/** A column that was never set keeps the old default, on. */ +function isOn(value: number | boolean | null): boolean { + return value === null || value === undefined ? true : !!value; +} + +export interface SshTerminalSettingsMigrationResult { + moved: string[]; + skipped: string[]; + hostsMoved: number; + hostsSkipped: number; +} + +export async function runSshTerminalSettingsMigration(): Promise<SshTerminalSettingsMigrationResult> { + const result: SshTerminalSettingsMigrationResult = { + moved: [], + skipped: [], + hostsMoved: 0, + hostsSkipped: 0, + }; + + try { + // plugin_settings has a foreign key to plugins: nothing to migrate into + // until the plugin row exists. + const plugin = await createCurrentPluginRepository().findById(PLUGIN_ID); + if (!plugin) return result; + + const settingsRepository = createCurrentSettingsRepository(); + const pluginSettingsRepository = createCurrentPluginSettingsRepository(); + + for (const move of ADMIN_MOVES) { + const legacyValue = await settingsRepository.get(move.legacyKey); + if (legacyValue === null || legacyValue === "") continue; + + const existing = await pluginSettingsRepository.get( + PLUGIN_ID, + "admin", + null, + move.field, + ); + if (existing && existing.value !== null) { + result.skipped.push(move.legacyKey); + continue; + } + + const parsed = move.parse(legacyValue); + if (parsed === undefined) { + result.skipped.push(move.legacyKey); + continue; + } + + const stored = move.secret + ? await encryptSystemSecret(String(parsed)) + : parsed; + await pluginSettingsRepository.set( + PLUGIN_ID, + "admin", + null, + move.field, + JSON.stringify(stored), + !!move.secret, + ); + result.moved.push(move.legacyKey); + } + + // Raw SQL, not the typed schema, so this keeps working once schema.ts + // stops declaring these columns. + const rows = await getDb().all<LegacyTerminalHostRow>(sql` + SELECT id, enable_terminal, enable_terminal_toolbar, enable_command_history + FROM ssh_data + WHERE enable_terminal = false + OR enable_terminal_toolbar = false + OR enable_command_history = false + `); + + for (const row of rows) { + const scopeId = String(row.id); + const existing = await pluginSettingsRepository.get( + PLUGIN_ID, + "host", + scopeId, + "enableTerminal", + ); + if (existing && existing.value !== null) { + result.hostsSkipped++; + continue; + } + + const values: Record<string, boolean> = { + enableTerminal: isOn(row.enable_terminal), + enableTerminalToolbar: isOn(row.enable_terminal_toolbar), + enableCommandHistory: isOn(row.enable_command_history), + }; + for (const [key, value] of Object.entries(values)) { + await pluginSettingsRepository.set( + PLUGIN_ID, + "host", + scopeId, + key, + JSON.stringify(value), + ); + } + result.hostsMoved++; + } + + if (result.moved.length > 0 || result.hostsMoved > 0) { + databaseLogger.info( + `Moved ${result.moved.length} terminal setting(s) and ${result.hostsMoved} host(s) into plugin settings`, + { + operation: "ssh_terminal_settings_migration", + moved: result.moved.join(", "), + hostsMoved: result.hostsMoved, + }, + ); + } + } catch (error) { + // A failed migration must not stop the backend. The terminal falls back + // to its defaults, which is a visible, recoverable state. + databaseLogger.warn("SSH terminal settings migration failed", { + operation: "ssh_terminal_settings_migration", + error: error instanceof Error ? error.message : String(error), + }); + } + + return result; +} diff --git a/src/backend/utils/login-event-dispatch.ts b/src/backend/utils/login-event-dispatch.ts deleted file mode 100644 index 4fc5e950c..000000000 --- a/src/backend/utils/login-event-dispatch.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { pluginEvents, TOPICS } from "../plugins/events.js"; -import { sshLogger } from "./logger.js"; - -/** - * Announces an SSH login so anything watching can react to it. - * - * Was an HTTP POST to the metrics service on localhost:30005. Host metrics is - * a plugin now and has no port, so this publishes on the plugin event bus and - * the plugin subscribes. Fire and forget: a subscriber that throws must never - * fail the login that caused it. - */ -export async function dispatchLoginEvent( - hostId: number, - userId: string, - sshUser: string, - fromIp: string, -): Promise<void> { - try { - pluginEvents.emit(TOPICS.hostLogin, { hostId, userId, sshUser, fromIp }); - } catch (err) { - sshLogger.warn("Failed to dispatch login event", { - operation: "login_event_dispatch_error", - hostId, - error: err instanceof Error ? err.message : String(err), - }); - } -} diff --git a/src/types/ui-types.ts b/src/types/ui-types.ts index d0a75a0df..edf830df2 100644 --- a/src/types/ui-types.ts +++ b/src/types/ui-types.ts @@ -337,6 +337,14 @@ export type Tab = { getApplicationCursorKeysMode?: () => boolean; openShareModal?: () => void; canShare?: () => boolean; + openFileManager?: () => void; + getShareTarget?: () => { + hostId: number; + sessionId: string; + protocol: "ssh"; + tabInstanceId?: string; + } | null; + focus?: () => void; } | null>; }; diff --git a/src/ui/AppShell.tsx b/src/ui/AppShell.tsx index a8e02adc0..2d0624dc3 100644 --- a/src/ui/AppShell.tsx +++ b/src/ui/AppShell.tsx @@ -1,5 +1,6 @@ /* eslint-disable react-refresh/only-export-components */ /* eslint-disable react-hooks/exhaustive-deps */ +import type { TabHandle } from "@termix/plugin-sdk/frontend"; import { toast } from "sonner"; import { useTranslation } from "react-i18next"; import { Separator } from "@/components/separator"; @@ -89,9 +90,21 @@ const CollabPanel = lazy(() => const MacrosPanel = lazy(() => import("@/sidebar/MacrosPanel").then((m) => ({ default: m.MacrosPanel })), ); -const HistoryPanel = lazy(() => - import("@/sidebar/HistoryPanel").then((m) => ({ default: m.HistoryPanel })), + +const ShareSessionModal = lazy(() => + import("@/features/session-sharing/ShareSessionModal").then((m) => ({ + default: m.ShareSessionModal, + })), ); + +/** What a session tab's handle gives the shell's share dialog. */ +interface ShareTarget { + hostId: number; + sessionId: string; + protocol: "ssh"; + tabInstanceId?: string; +} + const SessionLogsPanel = lazy(() => import("@/sidebar/SessionLogsPanel").then((m) => ({ default: m.SessionLogsPanel, @@ -514,10 +527,7 @@ export function AppShell({ const tabId = activeTabIdRef.current; if (!tabId) return; const termRef = terminalRefs.current.get(tabId); - ( - termRef?.current as - import("@/features/terminal/Terminal").TerminalHandle | null - )?.reconnect(); + (termRef?.current as TabHandle | null)?.reconnect?.(); return; } const currentTabs = tabsRef.current; @@ -608,9 +618,12 @@ export function AppShell({ // Once those panels can themselves be the active tab, activeTabId points at // the panel and the lookup misses, so remember the last terminal instead. const [lastTerminalTabId, setLastTerminalTabId] = useState(activeTabId); + const [shareTarget, setShareTarget] = useState<ShareTarget | null>(null); useEffect(() => { const active = tabs.find((t) => t.id === activeTabId); - if (active?.type === "terminal") setLastTerminalTabId(active.id); + if (active && getTabType(active.type)?.commandTarget) { + setLastTerminalTabId(active.id); + } }, [activeTabId, tabs]); useEffect(() => { splitModeRef.current = splitMode; @@ -856,10 +869,7 @@ export function AppShell({ const tabId = paneTabIdsRef.current[next]; if (tabId) { const termRef = terminalRefs.current.get(tabId); - ( - termRef?.current as - import("@/features/terminal/Terminal").TerminalHandle | null - )?.focus(); + (termRef?.current as TabHandle | null)?.focus?.(); } return; } @@ -1688,6 +1698,7 @@ 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); @@ -1758,26 +1769,24 @@ export function AppShell({ }); } - function openLocalTerminalTab(): string { + /** A tab type that opens a fresh tab every time (a local shell). */ + function openMultiInstanceTab(type: TabType): string { const instanceId = typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`; - const id = `local-terminal-${instanceId}`; + const id = `${type}-${instanceId}`; + const titleKey = getTabType(type)?.titleKey; + const title = titleKey ? t(titleKey) : type; setTabs((current) => { - const count = current.filter( - (tab) => tab.type === "local-terminal", - ).length; + const count = current.filter((tab) => tab.type === type).length; return [ ...current, { id, instanceId, - type: "local-terminal", - label: - count === 0 - ? t("nav.localTerminal") - : `${t("nav.localTerminal")} (${count + 1})`, + type, + label: count === 0 ? title : `${title} (${count + 1})`, openedAt: Date.now(), }, ]; @@ -1841,9 +1850,9 @@ export function AppShell({ host?: Host, data?: Record<string, unknown>, ) { - // Local terminals are never singletons, each one is its own shell. - if (type === "local-terminal") { - return openLocalTerminalTab(); + // A local shell is never a singleton: each open is its own session. + if (getTabType(type)?.multiInstance) { + return openMultiInstanceTab(type); } if (type === "host-manager") { if (pendingEvent === "host-manager:add-credential") { @@ -2024,8 +2033,15 @@ export function AppShell({ function openShareForTab(id: string) { const tab = tabs.find((t) => t.id === id); if (!tab) return; - const ref = tab.terminalRef?.current; - if (ref?.canShare?.()) { + const ref = tab.terminalRef?.current as TabHandle | null | undefined; + // A session tab either opens its own share dialog or hands over what + // the shell's dialog needs. + const target = ( + ref?.getShareTarget as (() => ShareTarget | null) | undefined + )?.(); + if (target) { + setShareTarget(target); + } else if (ref?.canShare?.()) { ref.openShareModal?.(); } else { toast.error(t("sessionSharing.notReadyToShare")); @@ -2311,8 +2327,7 @@ export function AppShell({ } for (const tab of tabs) { - const isTerminal = - tab.type === "terminal" || tab.type === "local-terminal"; + const isTerminal = !!getTabType(tab.type)?.ownBackground; const node = getTabNode(tab.id, isTerminal); const paneIdx = isSplit ? paneTabIds.indexOf(tab.id) : -1; const inPane = paneIdx !== -1; @@ -2373,7 +2388,7 @@ export function AppShell({ } }); - const terminalTabs = tabs.filter((t) => t.type === "terminal"); + const terminalTabs = tabs.filter((t) => getTabType(t.type)?.commandTarget); const topLevelTabs = tabs.filter((tab) => !tab.parentSplitTabId); function reorderTopLevelTabs(reordered: Tab[]) { @@ -2402,10 +2417,6 @@ export function AppShell({ */ // The param deliberately shadows the outer railView so the body reads the // same whichever dock is rendering. - const newInstanceId = () => - typeof crypto.randomUUID === "function" - ? crypto.randomUUID() - : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`; /** * What plugins may ask of the shell. Rebuilt every render so it always @@ -2423,12 +2434,7 @@ export function AppShell({ openSingletonTab(type, undefined, undefined, options?.data), closeTab: (tabId) => closeTab(tabId), renameTab: (tabId, label) => renameTab(tabId, label), - openTerminalTab: (host, path) => - openTab(host, "terminal", { - instanceId: newInstanceId(), - restoredSessionId: null, - initialFilePath: path, - }), + openRailView: (id) => { setRailView(id as RailView); setSidebarOpen(true); @@ -2540,15 +2546,6 @@ export function AppShell({ </div> )} - {railView === "history" && ( - <div className="flex flex-col flex-1 min-h-0 overflow-y-auto"> - <HistoryPanel - terminalTabs={terminalTabs} - activeTabId={targetTerminalTabId} - /> - </div> - )} - {railView === "split-screen" && ( <div className="flex-1 min-h-0 overflow-y-auto"> <SplitScreenPanel @@ -2579,6 +2576,9 @@ export function AppShell({ className={`flex flex-col flex-1 min-h-0 overflow-y-auto ${shown ? "" : "hidden"}`} > <Panel + targetTab={terminalTabs.find( + (tab) => tab.id === targetTerminalTabId, + )} active={shown} shell={panelShell} setEditing={setSidebarEditing} @@ -2673,16 +2673,10 @@ export function AppShell({ serverTunnels: [], quickActions: [], }; - const instanceId = - typeof crypto.randomUUID === "function" - ? crypto.randomUUID() - : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`; - openTab(host, "terminal", { - instanceId, - restoredSessionId: null, + void invokeAction("terminal.open", host, { joinSharedSessionId: session.sessionId, joinShareId: session.shareId, - savedLabel: t("connections.sharedSessionLabel", { + label: t("connections.sharedSessionLabel", { hostName: session.hostName, }), }); @@ -3053,7 +3047,7 @@ export function AppShell({ {tabsByPortalOrder.map((tab) => { const tabNode = getTabNode( tab.id, - tab.type === "terminal" || tab.type === "local-terminal", + !!getTabType(tab.type)?.ownBackground, ); const paneIdx = isSplit ? paneTabIds.indexOf(tab.id) : -1; const inPane = paneIdx !== -1; @@ -3129,6 +3123,19 @@ export function AppShell({ </div> </div> + {shareTarget && ( + <Suspense fallback={null}> + <ShareSessionModal + open + onClose={() => setShareTarget(null)} + hostId={shareTarget.hostId} + sessionId={shareTarget.sessionId} + protocol={shareTarget.protocol} + tabInstanceId={shareTarget.tabInstanceId} + /> + </Suspense> + )} + {commandPaletteOpen && ( <Suspense fallback={null}> <CommandPalette @@ -3148,8 +3155,8 @@ export function AppShell({ ].includes(type) ) { openSingletonTab(type, pendingEvent); - } else if (type === "local-terminal") { - openLocalTerminalTab(); + } else if (getTabType(type)?.multiInstance) { + openMultiInstanceTab(type); } else if (type === "tmux_monitor") { // --- tmux-monitor --- singleton tab, optionally preselecting a host openSingletonTab( diff --git a/src/ui/api/collab-api.ts b/src/ui/api/collab-api.ts index 1a767b134..9661e1145 100644 --- a/src/ui/api/collab-api.ts +++ b/src/ui/api/collab-api.ts @@ -47,6 +47,8 @@ export interface CollabRoomDetail { stage: CollabStage; controllerUserId: string | null; controlRequests: CollabControlRequest[]; + /** Where live room events are served, as /plugin-ws/<id>/<path>. */ + eventsWsPath: string; } export interface CollabControlRequest { diff --git a/src/ui/api/command-history-api.ts b/src/ui/api/command-history-api.ts deleted file mode 100644 index 57be8a4ca..000000000 --- a/src/ui/api/command-history-api.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { authApi, handleApiError } from "@/main-axios"; - -// COMMAND HISTORY API -// ============================================================================ - -export async function saveCommandToHistory( - hostId: number, - command: string, -): Promise<{ id: number; command: string; executedAt: string }> { - try { - const response = await authApi.post("/terminal/command_history", { - hostId, - command, - }); - return response.data; - } catch (error) { - throw handleApiError(error, "save command to history"); - } -} - -export async function getCommandHistory( - hostId: number, - limit: number = 100, -): Promise<string[]> { - try { - const response = await authApi.get(`/terminal/command_history/${hostId}`, { - params: { limit }, - }); - return response.data; - } catch (error) { - throw handleApiError(error, "fetch command history"); - } -} - -export async function deleteCommandFromHistory( - hostId: number, - command: string, -): Promise<{ success: boolean }> { - try { - const response = await authApi.post("/terminal/command_history/delete", { - hostId, - command, - }); - return response.data; - } catch (error) { - throw handleApiError(error, "delete command from history"); - } -} - -export async function clearCommandHistory( - hostId: number, -): Promise<{ success: boolean }> { - try { - const response = await authApi.delete( - `/terminal/command_history/${hostId}`, - ); - return response.data; - } catch (error) { - throw handleApiError(error, "clear command history"); - } -} - -// ============================================================================ diff --git a/src/ui/api/open-tabs-api.ts b/src/ui/api/open-tabs-api.ts index c4615b4b4..9a39a21fe 100644 --- a/src/ui/api/open-tabs-api.ts +++ b/src/ui/api/open-tabs-api.ts @@ -91,6 +91,12 @@ export async function getActiveSessions(): Promise<ActiveSessionInfo[]> { }); } +/** How long a detached terminal session is kept, in minutes. */ +export async function getSessionTimeoutMinutes(): Promise<number> { + const response = await authApi.get("/open-tabs/session-timeout"); + return Number(response.data?.minutes) || 30; +} + // ============================================================================ // USER PREFERENCES API // ============================================================================ diff --git a/src/ui/api/settings-api.ts b/src/ui/api/settings-api.ts index 3c580ce73..16525d10d 100644 --- a/src/ui/api/settings-api.ts +++ b/src/ui/api/settings-api.ts @@ -60,30 +60,6 @@ export async function getSessionTimeout(): Promise<{ timeoutHours: number }> { } } -// How long a detached terminal session is kept alive server-side. -export async function getTerminalSessionSettings(): Promise<{ - timeoutMinutes: number; - enabled: boolean; -}> { - try { - const response = await authApi.get("/terminal/session_settings"); - return response.data; - } catch (error) { - handleApiError(error, "fetch terminal session settings"); - } -} - -export async function updateTerminalSessionSettings(input: { - timeoutMinutes?: number; - enabled?: boolean; -}): Promise<void> { - try { - await authApi.patch("/terminal/session_settings", input); - } catch (error) { - handleApiError(error, "update terminal session settings"); - } -} - export interface StepCaSettings { configured: boolean; caUrl: string; @@ -190,79 +166,6 @@ export async function updateAnalyticsEnabled( // TERMINAL IMAGE STORAGE SETTINGS // ============================================================================ -export type TerminalImageStorageMode = "auto" | "local" | "remote-sftp"; - -/** Public settings shape: the backend-internal localDir is never returned. */ -export interface TerminalImageStorageSettings { - mode: TerminalImageStorageMode; - hostPath: string; - ttlMs: number; - maxCount: number; - maxBytes: number; - localMappingConfigured: boolean; -} - -export interface TerminalImageStorageSettingsUpdate { - mode?: TerminalImageStorageMode; - localDir?: string; - hostPath?: string; - ttlMs?: number; - maxCount?: number; - maxBytes?: number; -} - -export interface TerminalImageStorageTestResult { - mode: TerminalImageStorageMode; - connected: boolean; - remoteSftpAvailable: boolean; - localHostVisible: boolean | null; - selectedMode: "local" | "remote-sftp" | "unavailable"; - localMappingConfigured: boolean; -} - -export async function getTerminalImageStorageSettings(): Promise<TerminalImageStorageSettings> { - try { - const response = await authApi.get( - "/users/terminal-image-storage-settings", - ); - return response.data; - } catch (error) { - handleApiError(error, "fetch terminal image storage settings"); - } -} - -export async function updateTerminalImageStorageSettings( - settings: TerminalImageStorageSettingsUpdate, -): Promise<TerminalImageStorageSettings> { - try { - const response = await authApi.patch( - "/users/terminal-image-storage-settings", - settings, - ); - return response.data; - } catch (error) { - handleApiError(error, "update terminal image storage settings"); - } -} - -export async function testTerminalImageStorage( - instanceId: string, -): Promise<TerminalImageStorageTestResult> { - try { - const response = await authApi.post( - "/users/terminal-image-storage-settings/test", - { instanceId }, - ); - return response.data; - } catch (error) { - handleApiError(error, "test terminal image storage"); - } -} - -// ============================================================================ -// WHITE LABEL BRANDING -// ============================================================================ - export interface BrandingSettings { appName: string; tagline: string; diff --git a/src/ui/api/touch-input-settings-api.ts b/src/ui/api/touch-input-settings-api.ts deleted file mode 100644 index a34b435f0..000000000 --- a/src/ui/api/touch-input-settings-api.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { authApi, handleApiError } from "@/main-axios"; -import { - normalizeTouchInputSettings, - type TouchInputSettings, -} from "@/types/touch-input-settings"; - -export async function getTouchInputSettings(): Promise<TouchInputSettings> { - try { - const response = await authApi.get("/users/touch-input-settings"); - return normalizeTouchInputSettings(response.data); - } catch (error) { - handleApiError(error, "fetch touch input settings"); - } -} - -export async function updateTouchInputSettings( - settings: TouchInputSettings, -): Promise<TouchInputSettings> { - try { - const response = await authApi.patch( - "/users/touch-input-settings", - settings, - ); - return normalizeTouchInputSettings(response.data); - } catch (error) { - handleApiError(error, "update touch input settings"); - } -} diff --git a/src/ui/api/user-management-api.ts b/src/ui/api/user-management-api.ts index 2a1a93785..c5dc51862 100644 --- a/src/ui/api/user-management-api.ts +++ b/src/ui/api/user-management-api.ts @@ -343,30 +343,6 @@ export async function updatePasswordResetAllowed( } } -export async function getCommandHistoryEnabled(): Promise<{ - enabled: boolean; -}> { - try { - const response = await authApi.get("/users/command-history-enabled"); - return response.data; - } catch (error) { - handleApiError(error, "get command history enabled"); - } -} - -export async function updateCommandHistoryEnabled( - enabled: boolean, -): Promise<{ enabled: boolean }> { - try { - const response = await authApi.patch("/users/command-history-enabled", { - enabled, - }); - return response.data; - } catch (error) { - handleApiError(error, "update command history enabled"); - } -} - export async function updateOIDCConfig( config: Record<string, unknown>, ): Promise<Record<string, unknown>> { diff --git a/src/ui/features/terminal/TerminalPreview.tsx b/src/ui/components/terminal-preview/TerminalPreview.tsx similarity index 100% rename from src/ui/features/terminal/TerminalPreview.tsx rename to src/ui/components/terminal-preview/TerminalPreview.tsx diff --git a/src/ui/contexts/ConnectionDefaultsContext.tsx b/src/ui/contexts/ConnectionDefaultsContext.tsx index f9472a96f..2b8f24b1b 100644 --- a/src/ui/contexts/ConnectionDefaultsContext.tsx +++ b/src/ui/contexts/ConnectionDefaultsContext.tsx @@ -16,16 +16,12 @@ import { type TerminalDefaults, } from "@/lib/connection-defaults"; -type DefaultsKind = "terminal" | "rdp"; - interface ConnectionDefaultsContextValue { ready: boolean; terminal: TerminalDefaults; rdp: RemoteDesktopDefaults; - saveDefaults: ( - kind: DefaultsKind, - value: TerminalDefaults | RemoteDesktopDefaults, - ) => Promise<void>; + saveTerminalDefaults: (value: TerminalDefaults) => Promise<void>; + saveRdpDefaults: (value: RemoteDesktopDefaults) => Promise<void>; } const ConnectionDefaultsContext = createContext<ConnectionDefaultsContextValue>( @@ -33,7 +29,8 @@ const ConnectionDefaultsContext = createContext<ConnectionDefaultsContextValue>( ready: true, terminal: {}, rdp: {}, - saveDefaults: async () => {}, + saveTerminalDefaults: async () => {}, + saveRdpDefaults: async () => {}, }, ); @@ -63,22 +60,18 @@ export function ConnectionDefaultsProvider({ }; }, []); - const saveDefaults = useCallback( - async ( - kind: DefaultsKind, - value: TerminalDefaults | RemoteDesktopDefaults, - ) => { - const serialized = JSON.stringify(value); - await saveUserPreferences({ [`${kind}Defaults`]: serialized }); - if (kind === "terminal") setTerminal(value as TerminalDefaults); - if (kind === "rdp") setRdp(value as RemoteDesktopDefaults); - }, - [], - ); + const saveTerminalDefaults = useCallback(async (value: TerminalDefaults) => { + await saveUserPreferences({ terminalDefaults: JSON.stringify(value) }); + setTerminal(value); + }, []); + const saveRdpDefaults = useCallback(async (value: RemoteDesktopDefaults) => { + await saveUserPreferences({ rdpDefaults: JSON.stringify(value) }); + setRdp(value); + }, []); const value = useMemo( - () => ({ ready, terminal, rdp, saveDefaults }), - [ready, terminal, rdp, saveDefaults], + () => ({ ready, terminal, rdp, saveTerminalDefaults, saveRdpDefaults }), + [ready, terminal, rdp, saveTerminalDefaults, saveRdpDefaults], ); return ( <ConnectionDefaultsContext.Provider value={value}> diff --git a/src/ui/dashboard/DashboardTab.tsx b/src/ui/dashboard/DashboardTab.tsx index f506e5bdc..cdcfa07a9 100644 --- a/src/ui/dashboard/DashboardTab.tsx +++ b/src/ui/dashboard/DashboardTab.tsx @@ -384,7 +384,10 @@ function QuickActionsCard({ {pinnedHosts.slice(0, 4).map((host) => ( <button key={host.id} - onClick={() => onOpenTab(host, getDefaultConnectionTab(host))} + onClick={() => { + const type = getDefaultConnectionTab(host); + if (type) onOpenTab(host, type); + }} className="group/btn flex items-center gap-2.5 px-4 py-2 hover:bg-muted transition-colors cursor-pointer border-b border-border last:border-b-0" > <div className="size-7 border border-border bg-muted flex items-center justify-center shrink-0 group-hover/btn:bg-accent-brand/20 group-hover/btn:border-accent-brand/40 transition-colors"> diff --git a/src/ui/features/FullScreenAppWrapper.tsx b/src/ui/features/FullScreenAppWrapper.tsx index bcd319006..2f0fae277 100644 --- a/src/ui/features/FullScreenAppWrapper.tsx +++ b/src/ui/features/FullScreenAppWrapper.tsx @@ -1,6 +1,5 @@ import React, { useEffect, useState } from "react"; import { TabProvider } from "@/shell/TabContext.tsx"; -import { CommandHistoryProvider } from "@/features/terminal/command-history/CommandHistoryContext.tsx"; import { SidebarProvider } from "@/components/sidebar.tsx"; import { getSSHHosts, @@ -173,7 +172,7 @@ export const FullScreenAppWrapper: React.FC<FullScreenAppWrapperProps> = ({ return ( <SidebarProvider> <TabProvider> - <CommandHistoryProvider> + <> <div className="w-full h-screen overflow-hidden flex items-center justify-center" style={{ backgroundColor: "var(--bg-base)" }} @@ -187,7 +186,7 @@ export const FullScreenAppWrapper: React.FC<FullScreenAppWrapperProps> = ({ offset={20} /> </div> - </CommandHistoryProvider> + </> </TabProvider> </SidebarProvider> ); @@ -202,14 +201,14 @@ export const FullScreenAppWrapper: React.FC<FullScreenAppWrapperProps> = ({ return ( <SidebarProvider> <TabProvider> - <CommandHistoryProvider> + <> <div className="w-full h-screen overflow-hidden" style={{ backgroundColor: "var(--bg-base)" }} > {children(hostConfig, phase)} </div> - </CommandHistoryProvider> + </> </TabProvider> </SidebarProvider> ); diff --git a/src/ui/features/collab/CollabGuestView.tsx b/src/ui/features/collab/CollabGuestView.tsx index e3e0af835..3a0d9d0fc 100644 --- a/src/ui/features/collab/CollabGuestView.tsx +++ b/src/ui/features/collab/CollabGuestView.tsx @@ -93,7 +93,7 @@ export default function CollabGuestView() { <GuestTerminalView key={stage.shareId} share={{ permissionLevel: "read-only" }} - wsQuery={`roomGuestToken=${encodeURIComponent(token ?? "")}`} + wsPath={stage.wsPath ?? ""} hideBadges onParticipantsChange={(participants: SessionParticipantInfo[]) => setParticipantCount(participants.length) diff --git a/src/ui/features/collab/CollabRoomTab.tsx b/src/ui/features/collab/CollabRoomTab.tsx index aeedc3970..e6d11b0f7 100644 --- a/src/ui/features/collab/CollabRoomTab.tsx +++ b/src/ui/features/collab/CollabRoomTab.tsx @@ -1,4 +1,5 @@ import { useCallback, useEffect, useRef, useState } from "react"; +import { PluginComponent } from "@/plugin-host/component-registry"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { @@ -31,8 +32,6 @@ import { } from "@/components/alert-dialog"; import { Input } from "@/components/input"; import { CollabMembersSidebar } from "./CollabMembersSidebar"; -import { Terminal } from "@/features/terminal/Terminal"; -import { CommandHistoryProvider } from "@/features/terminal/command-history/CommandHistoryContext"; import { RemoteDisplay, createRemoteSessionToken, @@ -40,7 +39,7 @@ import { import { getSSHHosts, getUserList, type SSHHostWithStatus } from "@/main-axios"; import { getRoles } from "@/api/rbac-api"; import type { Role } from "@/main-axios"; -import { pluginWsUrl } from "@/lib/plugin-transport"; +import { pluginWsUrlForPath } from "@/lib/plugin-transport"; import { getErrorMessage } from "@/lib/error-message"; import { deleteCollabRoom, @@ -65,8 +64,9 @@ const POLL_FALLBACK_MS = 15000; // The terminal socket the ssh-terminal plugin serves. Authentication rides on // the jwt cookie the way every terminal WS connection does. -async function roomEventsWsUrl(): Promise<string | null> { - const target = await pluginWsUrl("ssh-terminal", "/terminal"); +async function roomEventsWsUrl(roomId: string): Promise<string | null> { + const { eventsWsPath } = await getCollabRoom(roomId); + const target = await pluginWsUrlForPath(eventsWsPath); return target?.url ?? null; } @@ -186,7 +186,7 @@ export function CollabRoomTab({ const connect = async () => { if (cancelled) return; try { - const url = await roomEventsWsUrl(); + const url = await roomEventsWsUrl(roomId); if (cancelled) return; if (!url) throw new Error("No terminal endpoint is available"); ws = new WebSocket(url); @@ -673,23 +673,22 @@ export function CollabRoomTab({ <div className="relative flex-1 min-w-0 min-h-0"> {draft ? ( draft.protocol === "ssh" ? ( - <CommandHistoryProvider> - <Terminal - hostConfig={{ - ...draft.host, - id: Number(draft.host.id), - ip: draft.host.ip, - port: draft.host.port, - username: draft.host.username, - instanceId: `collab-present-${roomId}`, - }} - isVisible={isVisible} - disableAutoFocus={false} - onSessionReady={(sessionId) => - void registerStage("ssh", sessionId, Number(draft.host.id)) - } - /> - </CommandHistoryProvider> + <PluginComponent + id="terminal.view" + hostConfig={{ + ...draft.host, + id: Number(draft.host.id), + ip: draft.host.ip, + port: draft.host.port, + username: draft.host.username, + instanceId: `collab-present-${roomId}`, + }} + isVisible={isVisible} + disableAutoFocus={false} + onSessionReady={(sessionId) => + void registerStage("ssh", sessionId, Number(draft.host.id)) + } + /> ) : ( <RemoteDisplay token={draft.token} @@ -716,23 +715,22 @@ export function CollabRoomTab({ </div> )} {stage.protocol === "ssh" ? ( - <CommandHistoryProvider> - <Terminal - hostConfig={{ - id: stage.hostId ?? undefined, - name: detail?.room.name ?? "stage", - ip: "", - port: 0, - username: "", - authType: "none", - instanceId: `collab-view-${roomId}-${stage.shareId}`, - joinShareId: stage.shareId, - joinSharedSessionId: stage.sessionId ?? null, - }} - isVisible={isVisible} - disableAutoFocus - /> - </CommandHistoryProvider> + <PluginComponent + id="terminal.view" + hostConfig={{ + id: stage.hostId ?? undefined, + name: detail?.room.name ?? "stage", + ip: "", + port: 0, + username: "", + authType: "none", + instanceId: `collab-view-${roomId}-${stage.shareId}`, + joinShareId: stage.shareId, + joinSharedSessionId: stage.sessionId ?? null, + }} + isVisible={isVisible} + disableAutoFocus + /> ) : stage.connectParams?.token ? ( <RemoteDisplay key={stage.connectParams.token} @@ -785,7 +783,7 @@ export function CollabRoomTab({ )} {filteredHosts.map((host) => { const protocols: Array<"ssh" | "rdp" | "vnc" | "telnet"> = []; - if (host.enableTerminal || host.enableSsh) protocols.push("ssh"); + if (host.enableSsh) protocols.push("ssh"); if (host.enableRdp) protocols.push("rdp"); if (host.enableVnc) protocols.push("vnc"); if (host.enableTelnet) protocols.push("telnet"); diff --git a/src/ui/features/homepage/widgets/QuickConnectWidget.tsx b/src/ui/features/homepage/widgets/QuickConnectWidget.tsx index 3aa78ac3a..8ba95aa36 100644 --- a/src/ui/features/homepage/widgets/QuickConnectWidget.tsx +++ b/src/ui/features/homepage/widgets/QuickConnectWidget.tsx @@ -38,10 +38,13 @@ function QuickConnectWidget({ const [loading, setLoading] = useState(true); const targets = quickConnectTargets(useHostActions()); + // An empty list means the first tools plugins offer, in their order. const types = config.connectionTypes.length > 0 ? config.connectionTypes - : (["terminal"] as QuickConnectType[]); + : (targets + .slice(0, 2) + .map((target) => target.type) as QuickConnectType[]); useEffect(() => { getSSHHosts() @@ -131,7 +134,7 @@ registerWidget<QuickConnectConfig>({ icon: <Zap size={14} />, defaultConfig: { hostIds: [], - connectionTypes: ["terminal", "files"], + connectionTypes: [], showStatus: true, layout: "list", }, diff --git a/src/ui/features/homepage/widgets/SshTerminalWidget.tsx b/src/ui/features/homepage/widgets/SshTerminalWidget.tsx index 6a749d145..4bfaabc6a 100644 --- a/src/ui/features/homepage/widgets/SshTerminalWidget.tsx +++ b/src/ui/features/homepage/widgets/SshTerminalWidget.tsx @@ -1,4 +1,5 @@ import { useState, useRef, useEffect } from "react"; +import { PluginComponent } from "@/plugin-host/component-registry"; import { Terminal as TerminalIcon, Play } from "lucide-react"; import { useTranslation } from "react-i18next"; import { registerWidget } from "./WidgetRegistry"; @@ -7,8 +8,7 @@ import type { WidgetComponentProps, } from "@/types/homepage-types"; import { GRID_SIZE } from "@/types/homepage-types"; -import { CommandHistoryProvider } from "@/features/terminal/command-history/CommandHistoryContext"; -import { Terminal, type TerminalHandle } from "@/features/terminal/Terminal"; +import type { TabHandle as TerminalHandle } from "@termix/plugin-sdk/frontend"; import { getSSHHosts } from "@/api/ssh-host-management-api"; import type { SSHHostWithStatus } from "@/main-axios"; import { WidgetTitle } from "./WidgetTitle"; @@ -136,17 +136,16 @@ function SshTerminalWidget({ > <WidgetTitle title={widget.title} icon={<TerminalIcon size={11} />} /> <div className="flex-1 overflow-hidden"> - <CommandHistoryProvider> - <Terminal - ref={terminalRef} - hostConfig={hostConfig} - isVisible={true} - showTitle={false} - splitScreen={false} - disableAutoFocus={true} - onClose={() => setStarted(false)} - /> - </CommandHistoryProvider> + <PluginComponent + id="terminal.view" + handleRef={terminalRef} + hostConfig={hostConfig} + isVisible={true} + showTitle={false} + splitScreen={false} + disableAutoFocus={true} + onClose={() => setStarted(false)} + /> </div> </div> ); diff --git a/src/ui/features/serial/Serial.tsx b/src/ui/features/serial/Serial.tsx index d052b2e20..e10ba236c 100644 --- a/src/ui/features/serial/Serial.tsx +++ b/src/ui/features/serial/Serial.tsx @@ -12,9 +12,9 @@ import { TriangleAlert } from "lucide-react"; import { isElectron } from "@/lib/electron"; import { websocketAuthProtocols } from "@/lib/ws-auth"; import { useTheme } from "@/components/theme-provider"; -import { resolveTermixThemeColors } from "@/features/terminal/terminal-theme"; +import { resolveTermixThemeColors } from "@/lib/terminal-look/terminal-theme"; import { DEFAULT_TERMINAL_CONFIG, TERMINAL_FONTS } from "@/lib/terminal-themes"; -import { ensureTerminalFontsLoaded } from "@/features/terminal/terminal-global-styles"; +import { ensureTerminalFontsLoaded } from "@/lib/terminal-look/terminal-global-styles"; import type { SerialConfig } from "@/types/ui-types"; import type { SerialHandle } from "./serial-types"; diff --git a/src/ui/features/session-sharing/SharedSessionView.tsx b/src/ui/features/session-sharing/SharedSessionView.tsx index ac7dbf282..69eb42f0e 100644 --- a/src/ui/features/session-sharing/SharedSessionView.tsx +++ b/src/ui/features/session-sharing/SharedSessionView.tsx @@ -9,7 +9,7 @@ import { type ShareLinkErrorKind, } from "@/api/session-sharing-api"; import { SimpleLoader } from "@/lib/SimpleLoader.tsx"; -import { pluginWsUrl } from "@/lib/plugin-transport"; +import { pluginWsUrlForPath } from "@/lib/plugin-transport"; import { RemoteDisplay } from "@/features/remote-display/RemoteDisplay"; const PING_INTERVAL_MS = 30000; @@ -25,8 +25,8 @@ interface TerminalWsMessage { // A shared session link is always resolved against the desktop app's // embedded local backend -- joining a session hosted on someone else's // remote server isn't supported from the desktop app today. -async function resolveTerminalWsBaseUrl(): Promise<string> { - const target = await pluginWsUrl("ssh-terminal", "/terminal"); +async function resolveTerminalWsUrl(wsPath: string): Promise<string> { + const target = await pluginWsUrlForPath(wsPath); if (!target) throw new Error("No terminal endpoint is available"); return target.url; } @@ -107,12 +107,13 @@ function CenteredMessage({ export function GuestTerminalView({ share, - wsQuery, + wsPath, hideBadges = false, onParticipantsChange, }: { share: Pick<ResolvedShareLink, "permissionLevel">; - wsQuery: string; + /** The socket path the backend handed out, query string included. */ + wsPath: string; /** Suppress the built-in overlay badges when the host page renders its own. */ hideBadges?: boolean; onParticipantsChange?: (participants: SessionParticipantInfo[]) => void; @@ -144,10 +145,9 @@ export function GuestTerminalView({ let cancelled = false; let ws: WebSocket | null = null; - resolveTerminalWsBaseUrl().then((baseWsUrl) => { + resolveTerminalWsUrl(wsPath).then((url) => { if (cancelled) return; - const separator = baseWsUrl.includes("?") ? "&" : "?"; - ws = new WebSocket(`${baseWsUrl}${separator}${wsQuery}`); + ws = new WebSocket(url); wsRef.current = ws; ws.onopen = () => { @@ -209,7 +209,7 @@ export function GuestTerminalView({ }; // Deliberately runs once terminal mounts - share/token/permission are stable for the view's lifetime. // eslint-disable-next-line react-hooks/exhaustive-deps - }, [terminal, wsQuery]); + }, [terminal, wsPath]); return ( <div className="relative w-full h-full"> @@ -354,10 +354,7 @@ export default function SharedSessionView() { share && linkToken && (share.protocol === "ssh" ? ( - <GuestTerminalView - share={share} - wsQuery={`shareToken=${encodeURIComponent(linkToken)}`} - /> + <GuestTerminalView share={share} wsPath={share.wsPath} /> ) : ( <GuestGuacamoleView share={share} /> ))} diff --git a/src/ui/features/tmux-monitor/PanePreview.tsx b/src/ui/features/tmux-monitor/PanePreview.tsx index 6b639f801..7bdf4c0ef 100644 --- a/src/ui/features/tmux-monitor/PanePreview.tsx +++ b/src/ui/features/tmux-monitor/PanePreview.tsx @@ -3,6 +3,7 @@ // 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 { @@ -15,12 +16,9 @@ import { Trash2, X, } from "lucide-react"; -import { CommandHistoryProvider } from "@/features/terminal/command-history/CommandHistoryContext"; -import { Terminal } from "@/features/terminal/Terminal"; -import type { - TerminalHandle, - TerminalHostConfig, -} from "@/features/terminal/Terminal"; +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"; @@ -154,18 +152,18 @@ export function PanePreview({ </div> </div> <div className="min-h-0 flex-1 overflow-hidden"> - <CommandHistoryProvider key={attachNonce}> - <Terminal - ref={terminalRef} - hostConfig={terminalHostConfig} - isVisible={true} - title={pane.sessionName} - showTitle={false} - splitScreen={false} - tmuxAttachSession={pane.sessionName} - onClose={onClose} - /> - </CommandHistoryProvider> + <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> </> ); diff --git a/src/ui/features/tmux-monitor/TmuxMonitor.tsx b/src/ui/features/tmux-monitor/TmuxMonitor.tsx index ad826a9b4..179c63771 100644 --- a/src/ui/features/tmux-monitor/TmuxMonitor.tsx +++ b/src/ui/features/tmux-monitor/TmuxMonitor.tsx @@ -1,4 +1,8 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { + defaultConnectAction, + listHostActions, +} from "@/sidebar/host-contributions"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { @@ -46,7 +50,7 @@ import { type TmuxSearchMatch, type TmuxSearchResult, } from "@/api/tmux-monitor-api"; -import type { TerminalHandle } from "@/features/terminal/Terminal"; +import type { TabHandle as TerminalHandle } from "@termix/plugin-sdk/frontend"; import { useAdaptivePolling } from "@/hooks/use-adaptive-polling"; import { SessionTree, type SessionMetricsAgg } from "./SessionTree"; import { SearchResults } from "./SearchResults"; @@ -184,7 +188,6 @@ export function TmuxMonitor({ const sshHosts = all.filter( (h) => (h.connectionType ?? "ssh") === "ssh" && - h.enableTerminal !== false && h.enableTmuxMonitor === true, ); setHosts(sshHosts); @@ -766,8 +769,17 @@ export function TmuxMonitor({ if (session) attachInline(session); return; } + // The standalone view of whichever plugin connects to a host by default. + const selectedHost = hosts.find((h) => h.id === selectedHostId); + const view = selectedHost + ? defaultConnectAction( + listHostActions(), + selectedHost as unknown as Parameters<typeof defaultConnectAction>[1], + )?.copyUrlView + : undefined; + if (!view) return; const params = new URLSearchParams({ - view: "terminal", + view, hostId: String(selectedHostId), }); if (session) params.set("tmuxSession", session); diff --git a/src/ui/lib/host-connection-tabs.ts b/src/ui/lib/host-connection-tabs.ts index 0a402d6b8..b2dda94d9 100644 --- a/src/ui/lib/host-connection-tabs.ts +++ b/src/ui/lib/host-connection-tabs.ts @@ -8,10 +8,11 @@ import { /** * Which tab a click on a host opens. The ways to connect (SSH terminal, RDP, * VNC, Telnet) are host actions that plugins register, so the answer is the - * highest-priority connect action the host allows. + * highest-priority connect action the host allows, or null when no running + * plugin can connect to it. */ -export function getDefaultConnectionTab(host: Host): TabType { - return defaultConnectAction(listHostActions(), host)?.tabType ?? "terminal"; +export function getDefaultConnectionTab(host: Host): TabType | null { + return defaultConnectAction(listHostActions(), host)?.tabType ?? null; } /** @@ -22,7 +23,7 @@ export function getDefaultConnectionTab(host: Host): TabType { export function resolveHostTabType( host: Host, preferredType?: TabType, -): TabType { +): TabType | null { if (!preferredType) return getDefaultConnectionTab(host); const all = listHostActions(); const isConnectType = all.some( diff --git a/src/ui/lib/plugin-transport.ts b/src/ui/lib/plugin-transport.ts index 5963b1e61..718e7a638 100644 --- a/src/ui/lib/plugin-transport.ts +++ b/src/ui/lib/plugin-transport.ts @@ -75,6 +75,23 @@ export function createPluginApi(pluginId: string): AxiosInstance { }) as AxiosInstance; } +/** + * pluginWsUrl for a path a backend handed out ("/plugin-ws/<id>/<path>?q"), + * so a caller can dial a plugin's socket without naming the plugin. + */ +export async function pluginWsUrlForPath( + wsPath: string, + options: { origin?: ConnectionOrigin } = {}, +): Promise<WebSocketConnectionTarget | null> { + const [pathname, query] = wsPath.split("?", 2); + const match = /^\/plugin-ws\/([^/]+)(\/.*)$/.exec(pathname); + if (!match) return null; + const target = await pluginWsUrl(match[1], match[2], options); + if (!target || !query) return target; + const separator = target.url.includes("?") ? "&" : "?"; + return { ...target, url: `${target.url}${separator}${query}` }; +} + /** * The WebSocket URL for /plugin-ws/<id>/<path>, plus the subprotocols that * carry the JWT. diff --git a/src/ui/features/terminal/terminal-font-zoom.ts b/src/ui/lib/terminal-look/terminal-font-zoom.ts similarity index 100% rename from src/ui/features/terminal/terminal-font-zoom.ts rename to src/ui/lib/terminal-look/terminal-font-zoom.ts diff --git a/src/ui/features/terminal/terminal-global-styles.ts b/src/ui/lib/terminal-look/terminal-global-styles.ts similarity index 100% rename from src/ui/features/terminal/terminal-global-styles.ts rename to src/ui/lib/terminal-look/terminal-global-styles.ts diff --git a/src/ui/features/terminal/terminal-theme.ts b/src/ui/lib/terminal-look/terminal-theme.ts similarity index 100% rename from src/ui/features/terminal/terminal-theme.ts rename to src/ui/lib/terminal-look/terminal-theme.ts diff --git a/src/ui/locales/en.json b/src/ui/locales/en.json index 88d938f64..b9d8c9f4a 100644 --- a/src/ui/locales/en.json +++ b/src/ui/locales/en.json @@ -738,7 +738,6 @@ "addHost": "Add Host", "editHost": "Edit Host", "cloneHost": "Clone Host", - "enableTerminal": "Enable Terminal", "enableTunnel": "Enable Tunnel", "enableFileManager": "Enable File Manager", "enableDocker": "Enable Docker", @@ -1037,8 +1036,6 @@ "enableSessionLoggingDesc": "Record terminal session output for later review", "allowSessionSharing": "Allow Session Sharing", "allowSessionSharingDesc": "Let live sessions on this host be shared via link or with other users", - "enableCommandHistory": "Command History", - "enableCommandHistoryDesc": "Record commands run in this terminal for history and autocomplete", "localEchoLabel": "Local Echo", "localEchoDesc": "Predict safe printable keystrokes locally on slow connections. Password prompts and control input are never echoed.", "localEchoDefault": "Use global setting", @@ -1068,8 +1065,6 @@ "bindHost": "Bind Host", "enableTmuxMonitor": "Enable Tmux Monitor", "enableTmuxMonitorDesc": "Show this host in the Tmux Monitor and add its tmux actions to the sidebar", - "enableTerminalToolbar": "Enable Terminal Toolbar", - "enableTerminalToolbarDesc": "Show a quick-access toolbar docked to the active terminal for this host's tools and live stats", "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.", @@ -1554,8 +1549,6 @@ "navigate": "Navigate", "select": "Select", "toggleWith": "Toggle with", - "localTerminal": "Local Terminal", - "localTerminalDesc": "Open a shell on this computer", "pluginActions": "From plugins" }, "splitScreen": { @@ -1721,57 +1714,13 @@ "loadSharesFailed": "Failed to load active shares" }, "terminal": { - "sessionExpiredNotice": "Your previous session on {{host}} expired after {{minutes}} minutes disconnected; the shell and anything running in it are gone. Enable Auto-Tmux on this host to keep sessions alive.", - "sessionExpiredNoticeNoMinutes": "Your previous session on {{host}} expired while disconnected; the shell and anything running in it are gone. Enable Auto-Tmux on this host to keep sessions alive.", - "enableAutoTmuxAction": "Enable Auto-Tmux", - "autoTmuxEnabled": "Auto-Tmux enabled for {{host}}. It applies from the next connection.", - "autoTmuxEnableFailed": "Failed to enable Auto-Tmux", "connect": "Connect to Host", "clear": "Clear", "paste": "Paste", - "reconnect": "Reconnect", - "connectionLost": "Connection lost", "connected": "Connected", "clipboardWriteFailed": "Failed to copy to clipboard. Make sure the page is served over HTTPS or localhost.", "clipboardReadFailed": "Failed to read from clipboard. Make sure clipboard permissions are granted.", - "clipboardHttpWarning": "Couldn't read the clipboard. Some browsers restrict clipboard access outside HTTPS - try Ctrl+V instead, or serve Termix over HTTPS.", - "passwordPromptFillTitle": "Fill the saved password into this prompt?", - "unknownError": "Unknown error occurred", - "websocketError": "WebSocket connection error", - "connecting": "Connecting...", - "noHostSelected": "No host selected", - "reconnecting": "Reconnecting... ({{attempt}}/{{max}})", - "reconnected": "Reconnected successfully", - "tmuxSessionCreated": "tmux session created: {{name}}", - "tmuxSessionAttached": "tmux session attached: {{name}}", - "tmuxUnavailable": "tmux is not installed on the remote host, falling back to standard shell", - "tmuxSessionPickerTitle": "tmux Sessions", - "tmuxSessionPickerDesc": "Existing tmux sessions found on this host. Select one to reattach or create a new session.", - "tmuxWindows": "Windows", - "tmuxWindowCount": "{{count}} window", - "tmuxAttached": "Attached clients", - "tmuxAttachedCount": "{{count}} attached", - "tmuxLastActivity": "Last activity", - "tmuxTimeJustNow": "just now", - "tmuxTimeMinutes": "{{count}}m ago", - "tmuxTimeHours": "{{count}}h ago", - "tmuxTimeDays": "{{count}}d ago", - "tmuxCreateNew": "Start new session", - "tmuxCopyHint": "Adjust selection and press Enter to copy to clipboard", "tmuxDetach": "Detach from tmux session", - "tmuxDetached": "Detached from tmux session", - "searchPlaceholder": "Find", - "searchCaseSensitive": "Match Case", - "searchWholeWord": "Match Whole Word", - "searchRegex": "Use Regular Expression", - "searchNoResults": "No results", - "searchResultCount": "{{index}} of {{count}}", - "searchNext": "Next Match (Enter)", - "searchPrevious": "Previous Match (Shift+Enter)", - "searchClose": "Close (Escape)", - "maxReconnectAttemptsReached": "Maximum reconnection attempts reached", - "closeTab": "Close", - "connectionTimeout": "Connection timeout", "terminalTitle": "Terminal - {{host}}", "terminalWithPath": "Terminal - {{host}}:{{path}}", "runTitle": "Running {{command}} - {{host}}", @@ -1795,27 +1744,14 @@ "opksshOpenBrowser": "Open Browser to Authenticate", "opksshWaitingForAuth": "Waiting for authentication in browser...", "opksshAuthenticating": "Processing authentication...", - "opksshTimeout": "Authentication timed out. Please try again.", - "opksshAuthFailed": "Authentication failed. Please check your credentials and try again.", "opksshSignInWith": "Sign in with {{provider}}", - "vaultAuthTitle": "Vault sign-in required", - "vaultAuthDescription": "A window has opened to sign in to HashiCorp Vault. Complete the sign-in there; this connection will continue automatically.", - "vaultAuthFailed": "Vault authentication failed. Please try again.", - "vaultReopen": "Reopen sign-in window", "sudoPasswordPopupTitle": "Insert Password?", - "linkDialogTitle": "Open Link", - "linkDialogOpen": "Open", - "linkDialogCopy": "Copy", - "websocketAbnormalClose": "Connection closed unexpectedly. This may be due to a reverse proxy or SSL configuration issue. Please check server logs.", "connectionLogTitle": "Connection Log", "connectionLogCopy": "Copy logs to clipboard", "connectionLogEmpty": "No connection logs yet", "connectionLogWaiting": "Waiting for connection logs...", "connectionLogCopied": "Connection logs copied to clipboard", "connectionLogCopyFailed": "Failed to copy logs to clipboard", - "connectionRejected": "Connection rejected by server. Please check your authentication and network configuration.", - "hostKeyRejected": "SSH host key verification rejected. Connection cancelled.", - "sessionTakenOver": "Session was opened in another tab. Reconnecting...", "split": { "splitTab": "Split Tab", "addToSplit": "Add to Split", @@ -2237,55 +2173,6 @@ "sectionApiKeys": "API Keys", "sectionAuditLog": "Audit Log", "sectionSsl": "SSL / Let's Encrypt", - "sectionTouchInput": "Touch Input", - "touchEnabled": "Enable Touch Input", - "touchEnabledDesc": "Allow one-finger touch scrolling in terminals", - "touchMomentumEnabled": "Enable Momentum", - "touchMomentumEnabledDesc": "Continue scrolling briefly after a swipe", - "touchAdvanced": "Advanced", - "touchDragThreshold": "Drag threshold", - "touchMaxWheelDelta": "Maximum wheel delta", - "touchMomentumSampleWindow": "Momentum sample window", - "touchReleaseGracePeriod": "Release grace period", - "touchMinimumVelocity": "Minimum velocity", - "touchMaximumVelocity": "Maximum velocity", - "touchMaximumDuration": "Maximum duration", - "touchMaximumTravel": "Maximum travel", - "touchDecayTime": "Decay time", - "touchPixelsPerTick": "Pixels per tick", - "touchMaximumFrameInterval": "Maximum frame interval", - "touchMaximumTicksPerFrame": "Maximum ticks per frame", - "touchResetDefaults": "Reset to defaults", - "touchSaved": "Touch input settings saved", - "touchSaveFailed": "Failed to save touch input settings", - "sectionImageStorage": "Image Storage", - "imageStorageMode": "Storage Mode", - "imageStorageModeDesc": "Where pasted and uploaded terminal images are written. Auto picks by connection, Local and Remote (SFTP) are fixed.", - "imageStorageModeAuto": "Auto", - "imageStorageModeLocal": "Local", - "imageStorageModeRemoteSftp": "Remote (SFTP)", - "imageStorageLocalDir": "Local directory", - "imageStorageLocalDirDesc": "Absolute path on the Termix backend. Not shown after saving for security.", - "imageStorageLocalDirPlaceholder": "Leave empty to keep current", - "imageStorageHostPath": "Host path", - "imageStorageHostPathDesc": "The host-side view of the local directory that terminal agents receive.", - "imageStorageTtl": "TTL (ms)", - "imageStorageMaxCount": "Max image count", - "imageStorageMaxBytes": "Max storage bytes", - "imageStorageSaved": "Image storage settings saved", - "imageStorageSaveFailed": "Failed to save image storage settings", - "imageStorageInstanceId": "Terminal session instance ID", - "imageStorageInstanceIdDesc": "Instance ID of one of your connected terminal tabs, used to test visibility.", - "imageStorageInstanceIdRequired": "Enter a terminal session instance ID first", - "imageStorageTest": "Test", - "imageStorageTesting": "Testing...", - "imageStorageTestFailed": "Failed to test image storage", - "imageStorageTestConnected": "Terminal session is connected.", - "imageStorageTestNotConnected": "No connected terminal session with this instance ID.", - "imageStorageTestSelectedMode": "An upload would use: {{mode}}", - "imageStorageSelectedModeUnavailable": "Unavailable", - "imageStorageTestLocalVisible": "The local directory is visible from the connected host.", - "imageStorageTestLocalNotVisible": "The local directory is NOT visible from the connected host.", "sectionBranding": "White Label", "brandingAppName": "Application Name", "brandingAppNameDesc": "Shown on the login screen and browser tab. Leave empty to use \"Termix\".", @@ -2368,9 +2255,6 @@ "oidcSilentLoginDefaultLockedDesc": "This setting is locked by the OIDC_SILENT_LOGIN_DEFAULT environment variable and cannot be changed here.", "allowPasswordReset": "Allow Password Reset", "allowPasswordResetDesc": "Reset code via Docker logs", - "commandHistoryEnabled": "Command History", - "commandHistoryEnabledDesc": "Allow command history recording. When disabled, history is not saved regardless of per-host settings.", - "updateCommandHistoryFailed": "Failed to update command history setting", "analyticsEnabled": "Share Anonymous Usage Statistics", "analyticsEnabledDesc": "Sends an anonymous daily count of users, hosts, and feature usage to help improve Termix. No personal data or connection details are ever included.", "analyticsEnabledLockedDesc": "This setting is locked by the ENABLE_TELEMETRY environment variable and cannot be changed here.", @@ -2655,11 +2539,6 @@ "hostDefaultsTerminal": "Terminal", "hostDefaultsAutoTmux": "Auto-Tmux", "hostDefaultsAutoTmuxDesc": "Run new hosts' terminals inside tmux so sessions survive disconnects (needs tmux on the host)", - "terminalSessionTimeout": "Terminal Session Persistence", - "terminalSessionTimeoutDesc": "How long a disconnected terminal is kept alive on the server before it is closed. Min 1 · Max 1440 minutes. For sessions that must outlive this, enable Auto-Tmux on the host.", - "terminalSessionTimeoutRange": "Enter a value between 1 and 1440 minutes", - "terminalSessionTimeoutSaved": "Terminal session persistence saved", - "terminalSessionTimeoutSaveFailed": "Failed to save terminal session persistence", "minutes": "minutes", "hostDefaultsSessionLogging": "Session Logging", "hostDefaultsSessionLoggingDesc": "Record terminal sessions on new hosts by default", @@ -2743,15 +2622,6 @@ "connectToTerminal": "Connect to Terminal", "connectToFiles": "Connect to Files" }, - "history": { - "noTerminalSelected": "No terminal selected", - "noTerminalSelectedHint": "Open an SSH terminal tab to view its command history", - "searchPlaceholder": "Search history...", - "clearAll": "Clear All", - "noHistoryEntries": "No history entries", - "trackingDisabled": "History tracking is disabled", - "trackingDisabledHint": "Enable it in the host's terminal settings." - }, "sshTools": { "keyRecordingTitle": "Key Recording", "recordToTerminals": "Record to terminals", @@ -3217,30 +3087,11 @@ "terminalToolbar": { "title": "Terminal Toolbar", "showActions": "Show toolbar", - "hideToolbar": "Hide toolbar", - "showToolbar": "Expand toolbar", - "moveToolbar": "Move toolbar", - "moveToolbarHint": "Drag to reposition the toolbar", - "layout": "Toolbar display mode", - "layoutIcon": "Icon only", - "layoutLabeled": "Icons and labels", - "layoutExpanded": "Expanded with metrics", - "image": "Image actions (images only)", - "upload": "Upload image", - "paste": "Paste image", - "detachTmux": "Detach tmux", - "detachTmuxDescription": "Detach tmux session", - "imageActionFailed": "Image action failed", - "retry": "Retry", - "dismissError": "Dismiss image error", "cpu": "CPU", "memory": "Memory", "disk": "Disk", "statsUnavailable": "Live stats unavailable", "tmuxDetach": "Detach tmux session", - "uploadImage": "Upload an image to the host", - "uploadingImage": "Uploading...", - "pasteImage": "Paste image from clipboard (images only)", "openFiles": "Open Files", "openDocker": "Open Docker", "openTunnel": "Open Tunnel", @@ -3324,31 +3175,6 @@ "timeHours": "{{count}}h ago", "timeDays": "{{count}}d ago" }, - "mobileKeyboard": { - "shift": "Shift", - "ctrl": "Ctrl", - "esc": "Esc", - "tab": "Tab", - "backTab": "⇥", - "arrowUp": "Arrow Up", - "arrowDown": "Arrow Down", - "arrowLeft": "Arrow Left", - "arrowRight": "Arrow Right", - "home": "Home", - "end": "End", - "pageUp": "PgUp", - "pageDown": "PgDn", - "delete": "Del", - "paste": "Paste", - "editQuickKeys": "Edit quick keys", - "quickKeysTitle": "Quick Keys", - "quickKeysDesc": "Tap × to remove. Supports up to 8 characters.", - "quickKeyPlaceholder": "e.g. sudo ", - "addQuickKey": "Add", - "removeQuickKey": "Remove", - "resetDefaults": "Reset to defaults", - "done": "Done" - }, "serial": { "title": "Serial", "portLabel": "Port", diff --git a/src/ui/locales/translated/af_ZA.json b/src/ui/locales/translated/af_ZA.json index 21db4ba96..597bfd885 100644 --- a/src/ui/locales/translated/af_ZA.json +++ b/src/ui/locales/translated/af_ZA.json @@ -742,7 +742,6 @@ "addHost": "Voeg gasheer by", "editHost": "Wysig gasheer", "cloneHost": "Kloon gasheer", - "enableTerminal": "Aktiveer Terminaal", "enableTunnel": "Aktiveer Tonnel", "enableFileManager": "Aktiveer Lêerbestuurder", "enableDocker": "Aktiveer Docker", @@ -1041,8 +1040,6 @@ "enableSessionLoggingDesc": "Neem terminaalsessie-uitvoer op vir latere hersiening", "allowSessionSharing": "Laat Sessiedeling Toe", "allowSessionSharingDesc": "Laat regstreekse sessies op hierdie gasheer via skakel of met ander gebruikers gedeel word", - "enableCommandHistory": "Opdraggeskiedenis", - "enableCommandHistoryDesc": "Rekordopdragte word in hierdie terminaal uitgevoer vir geskiedenis en outovoltooiing", "localEchoLabel": "Plaaslike Eggo", "localEchoDesc": "Voorspel veilige drukbare toetsaanslagen plaaslik op stadige verbindings. Wagwoordaanwysings en beheerinvoer word nooit herhaal nie.", "localEchoDefault": "Gebruik globale instelling", @@ -1072,8 +1069,6 @@ "bindHost": "Bind gasheer", "enableTmuxMonitor": "Aktiveer Tmux-monitor", "enableTmuxMonitorDesc": "Wys hierdie gasheer in die Tmux Monitor en voeg sy tmux-aksies by die sybalk.", - "enableTerminalToolbar": "Aktiveer Terminale-nutsbalk", - "enableTerminalToolbarDesc": "Wys 'n vinnige toegangsbalk wat aan die aktiewe terminaal gekoppel is vir hierdie gasheer se gereedskap en regstreekse statistieke.", "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.", @@ -1550,9 +1545,7 @@ "links": "Skakels", "navigate": "Navigeer", "select": "Kies", - "toggleWith": "Wissel met", - "localTerminal": "Plaaslike Terminaal", - "localTerminalDesc": "Maak 'n dop op hierdie rekenaar oop" + "toggleWith": "Wissel met" }, "splitScreen": { "paneEmpty": "Ruit {{index}} - leeg", @@ -1717,57 +1710,13 @@ "loadSharesFailed": "Kon nie aktiewe delings laai nie" }, "terminal": { - "sessionExpiredNotice": "Jou vorige sessie op {{host}} het verval nadat {{minutes}} minute ontkoppel was; die dop en enigiets wat daarin loop, is weg. Aktiveer Auto-Tmux op hierdie gasheer om sessies aan die gang te hou.", - "sessionExpiredNoticeNoMinutes": "Jou vorige sessie op {{host}} het verval terwyl dit ontkoppel was; die dop en enigiets wat daarin loop, is weg. Aktiveer Auto-Tmux op hierdie gasheer om sessies aan die gang te hou.", - "enableAutoTmuxAction": "Aktiveer Auto-Tmux", - "autoTmuxEnabled": "Auto-Tmux geaktiveer vir {{host}}. Dit geld vanaf die volgende verbinding.", - "autoTmuxEnableFailed": "Kon nie Auto-Tmux aktiveer nie", "connect": "Koppel aan gasheer", "clear": "Duidelik", "paste": "Plak", - "reconnect": "Herkoppel", - "connectionLost": "Verbinding verloor", "connected": "Verbonde", "clipboardWriteFailed": "Kon nie na knipbord kopieer nie. Maak seker dat die bladsy oor HTTPS of localhost bedien word.", "clipboardReadFailed": "Kon nie vanaf knipbord lees nie. Maak seker dat knipbordtoestemmings toegestaan is.", - "clipboardHttpWarning": "Kon nie die knipbord lees nie. Sommige blaaiers beperk toegang tot die knipbord buite HTTPS - probeer eerder Ctrl+V, of bedien Termix oor HTTPS.", - "passwordPromptFillTitle": "Vul die gestoorde wagwoord in hierdie aanwysingsprompt in?", - "unknownError": "Onbekende fout het voorgekom", - "websocketError": "WebSocket-verbindingsfout", - "connecting": "Verbind...", - "noHostSelected": "Geen gasheer gekies nie", - "reconnecting": "Herverbind... ({{attempt}}/{{max}})", - "reconnected": "Herverbind suksesvol", - "tmuxSessionCreated": "tmux-sessie geskep: {{name}}", - "tmuxSessionAttached": "tmux-sessie aangeheg: {{name}}", - "tmuxUnavailable": "tmux is nie op die afgeleë gasheer geïnstalleer nie, en val terug na die standaard dop.", - "tmuxSessionPickerTitle": "tmux-sessies", - "tmuxSessionPickerDesc": "Bestaande tmux-sessies is op hierdie gasheer gevind. Kies een om weer aan te heg of skep 'n nuwe sessie.", - "tmuxWindows": "Vensters", - "tmuxWindowCount": "{{count}} venster", - "tmuxAttached": "Aangehegte kliënte", - "tmuxAttachedCount": "{{count}} aangeheg", - "tmuxLastActivity": "Laaste aktiwiteit", - "tmuxTimeJustNow": "nou net", - "tmuxTimeMinutes": "{{count}}m gelede", - "tmuxTimeHours": "{{count}}uur gelede", - "tmuxTimeDays": "{{count}}dae gelede", - "tmuxCreateNew": "Begin nuwe sessie", - "tmuxCopyHint": "Pas seleksie aan en druk Enter om na knipbord te kopieer", "tmuxDetach": "Ontkoppel van tmux-sessie", - "tmuxDetached": "Losgemaak van tmux-sessie", - "searchPlaceholder": "Vind", - "searchCaseSensitive": "Pasmaat", - "searchWholeWord": "Pas die hele woord by", - "searchRegex": "Gebruik Gereelde Uitdrukking", - "searchNoResults": "Geen resultate nie", - "searchResultCount": "{{index}} van {{count}}", - "searchNext": "Volgende Wedstryd (Enter)", - "searchPrevious": "Vorige Wedersydse (Shift+Enter)", - "searchClose": "Maak toe (Escape)", - "maxReconnectAttemptsReached": "Maksimum herverbindingspogings bereik", - "closeTab": "Maak toe", - "connectionTimeout": "Verbindingstydverstryking", "terminalTitle": "Terminaal - {{host}}", "terminalWithPath": "Terminaal - {{host}}:{{path}}", "runTitle": "Hardloop {{command}} - {{host}}", @@ -1790,27 +1739,14 @@ "opksshOpenBrowser": "Maak blaaier oop om te verifieer", "opksshWaitingForAuth": "Wag vir verifikasie in blaaier...", "opksshAuthenticating": "Verwerk verifikasie...", - "opksshTimeout": "Verifikasie het verstryk. Probeer asseblief weer.", - "opksshAuthFailed": "Verifikasie het misluk. Kontroleer asseblief jou geloofsbriewe en probeer weer.", "opksshSignInWith": "Teken aan met {{provider}}", - "vaultAuthTitle": "Kluisaanmelding vereis", - "vaultAuthDescription": "'n Venster het oopgemaak om by HashiCorp Vault aan te meld. Voltooi die aanmelding daar; hierdie verbinding sal outomaties voortgaan.", - "vaultAuthFailed": "Kluisverifikasie het misluk. Probeer asseblief weer.", - "vaultReopen": "Maak aanmeldvenster weer oop", "sudoPasswordPopupTitle": "Wagwoord invoeg?", - "linkDialogTitle": "Maak skakel oop", - "linkDialogOpen": "Oop", - "linkDialogCopy": "Kopieer", - "websocketAbnormalClose": "Verbinding het onverwags gesluit. Dit kan wees as gevolg van 'n omgekeerde proxy- of SSL-konfigurasieprobleem. Gaan asseblief bedienerlogboeke na.", "connectionLogTitle": "Verbindingslogboek", "connectionLogCopy": "Kopieer logs na knipbord", "connectionLogEmpty": "Geen verbindingslogboeke nog nie", "connectionLogWaiting": "Wag vir verbindingslogboeke...", "connectionLogCopied": "Verbindingslogboeke na knipbord gekopieer", "connectionLogCopyFailed": "Kon nie logs na knipbord kopieer nie", - "connectionRejected": "Verbinding deur bediener verwerp. Kontroleer asseblief u verifikasie en netwerkkonfigurasie.", - "hostKeyRejected": "SSH-gasheersleutelverifikasie verwerp. Verbinding gekanselleer.", - "sessionTakenOver": "Sessie is in 'n ander oortjie oopgemaak. Herverbind tans...", "split": { "splitTab": "Verdeel oortjie", "addToSplit": "Voeg by Split", @@ -2225,55 +2161,6 @@ "sectionApiKeys": "API-sleutels", "sectionAuditLog": "Ouditlogboek", "sectionSsl": "SSL / Kom Ons Enkripteer", - "sectionTouchInput": "Aanraakinvoer", - "touchEnabled": "Aktiveer Aanraakinvoer", - "touchEnabledDesc": "Laat eenvinger-aanraakblaai in terminale toe", - "touchMomentumEnabled": "Aktiveer Momentum", - "touchMomentumEnabledDesc": "Gaan voort met blaai kortliks na 'n swipe", - "touchAdvanced": "Gevorderd", - "touchDragThreshold": "Sleepdrempel", - "touchMaxWheelDelta": "Maksimum wieldelta", - "touchMomentumSampleWindow": "Momentum voorbeeldvenster", - "touchReleaseGracePeriod": "Vrystellingsgrasietydperk", - "touchMinimumVelocity": "Minimum snelheid", - "touchMaximumVelocity": "Maksimum snelheid", - "touchMaximumDuration": "Maksimum duur", - "touchMaximumTravel": "Maksimum reis", - "touchDecayTime": "Vervaltyd", - "touchPixelsPerTick": "Piksels per tik", - "touchMaximumFrameInterval": "Maksimum raaminterval", - "touchMaximumTicksPerFrame": "Maksimum aantal tikkies per raam", - "touchResetDefaults": "Stel terug na verstekwaardes", - "touchSaved": "Aanraakinvoerinstellings gestoor", - "touchSaveFailed": "Kon nie raakinvoerinstellings stoor nie", - "sectionImageStorage": "Beeldberging", - "imageStorageMode": "Bergingsmodus", - "imageStorageModeDesc": "Waar geplakte en opgelaaide terminaalbeelde geskryf word. Outomatiese seleksies per verbinding, Plaaslik en Afgeleë (SFTP) is reggestel.", - "imageStorageModeAuto": "Outomaties", - "imageStorageModeLocal": "Plaaslik", - "imageStorageModeRemoteSftp": "Afstandsbediening (SFTP)", - "imageStorageLocalDir": "Plaaslike gids", - "imageStorageLocalDirDesc": "Absolute pad op die Termix-agterkant. Nie getoon na stoor vir sekuriteit nie.", - "imageStorageLocalDirPlaceholder": "Los leeg om op datum te bly", - "imageStorageHostPath": "Gasheerpad", - "imageStorageHostPathDesc": "Die gasheer-aansig van die plaaslike gids wat terminaalagente ontvang.", - "imageStorageTtl": "TTL (ms)", - "imageStorageMaxCount": "Maksimum beeldtelling", - "imageStorageMaxBytes": "Maksimum stoorplekgrepe", - "imageStorageSaved": "Beeldbergingsinstellings gestoor", - "imageStorageSaveFailed": "Kon nie beeldbergingsinstellings stoor nie", - "imageStorageInstanceId": "Terminale sessie-instansie-ID", - "imageStorageInstanceIdDesc": "Instansie-ID van een van jou gekoppelde terminaal-oortjies, wat gebruik word om sigbaarheid te toets.", - "imageStorageInstanceIdRequired": "Voer eers 'n terminaalsessie-instansie-ID in", - "imageStorageTest": "Toets", - "imageStorageTesting": "Toetsing...", - "imageStorageTestFailed": "Kon nie beeldberging toets nie", - "imageStorageTestConnected": "Terminaalsessie is gekoppel.", - "imageStorageTestNotConnected": "Geen gekoppelde terminaalsessie met hierdie instansie-ID nie.", - "imageStorageTestSelectedMode": "'n Oplaai sal gebruik: {{mode}}", - "imageStorageSelectedModeUnavailable": "Nie beskikbaar nie", - "imageStorageTestLocalVisible": "Die plaaslike gids is sigbaar vanaf die gekoppelde gasheer.", - "imageStorageTestLocalNotVisible": "Die plaaslike gids is NIE sigbaar vanaf die gekoppelde gasheer nie.", "sectionBranding": "Wit Etiket", "brandingAppName": "Toepassingsnaam", "brandingAppNameDesc": "Word op die aanmeldskerm en blaaier-oortjie getoon. Los leeg om \"Termix\" te gebruik.", @@ -2354,9 +2241,6 @@ "oidcSilentLoginDefaultLockedDesc": "Hierdie instelling word gesluit deur die OIDC_SILENT_LOGIN_DEFAULT omgewingveranderlike en kan nie hier verander word nie.", "allowPasswordReset": "Laat wagwoordherstel toe", "allowPasswordResetDesc": "Herstel kode via Docker-logboeke", - "commandHistoryEnabled": "Opdraggeskiedenis", - "commandHistoryEnabledDesc": "Laat die opname van opdraggeskiedenis toe. Wanneer dit gedeaktiveer is, word geskiedenis nie gestoor nie, ongeag die instellings per gasheer.", - "updateCommandHistoryFailed": "Kon nie die instelling vir opdraggeskiedenis opdateer nie", "analyticsEnabled": "Deel Anonieme Gebruiksstatistieke", "analyticsEnabledDesc": "Stuur 'n anonieme daaglikse telling van gebruikers, gashere en funksiegebruik om Termix te help verbeter. Geen persoonlike data of verbindingsbesonderhede word ooit ingesluit nie.", "analyticsEnabledLockedDesc": "Hierdie instelling word gesluit deur die ENABLE_TELEMETRY omgewingveranderlike en kan nie hier verander word nie.", @@ -2647,11 +2531,6 @@ "hostDefaultsTerminal": "Terminaal", "hostDefaultsAutoTmux": "Auto-Tmux", "hostDefaultsAutoTmuxDesc": "Laat nuwe gashere se terminale binne tmux loop sodat sessies ontkoppelings oorleef (benodig tmux op die gasheer)", - "terminalSessionTimeout": "Terminale Sessie Persistensie", - "terminalSessionTimeoutDesc": "Hoe lank 'n ontkoppelde terminaal op die bediener aan die gang gehou word voordat dit gesluit word. Min 1 · Maks 1440 minute. Vir sessies wat dit moet oorleef, aktiveer Auto-Tmux op die gasheer.", - "terminalSessionTimeoutRange": "Voer 'n waarde tussen 1 en 1440 minute in", - "terminalSessionTimeoutSaved": "Terminaalsessie-volhoubaarheid gestoor", - "terminalSessionTimeoutSaveFailed": "Kon nie die persistensie van die terminaalsessie stoor nie", "minutes": "minute", "hostDefaultsSessionLogging": "Sessie-logging", "hostDefaultsSessionLoggingDesc": "Neem standaard terminaalsessies op nuwe gashere op", @@ -2708,15 +2587,6 @@ "connectToTerminal": "Koppel aan terminaal", "connectToFiles": "Koppel aan lêers" }, - "history": { - "noTerminalSelected": "Geen terminaal gekies nie", - "noTerminalSelectedHint": "Maak 'n SSH-terminaal-oortjie oop om die opdraggeskiedenis te sien", - "searchPlaceholder": "Soekgeskiedenis...", - "clearAll": "Vee alles uit", - "noHistoryEntries": "Geen geskiedenisinskrywings nie", - "trackingDisabled": "Geskiedenisopsporing is gedeaktiveer", - "trackingDisabledHint": "Aktiveer dit in die gasheer se terminaalinstellings." - }, "sshTools": { "keyRecordingTitle": "Sleutelopname", "recordToTerminals": "Neem op na terminale", @@ -3178,30 +3048,11 @@ "terminalToolbar": { "title": "Terminale-nutsbalk", "showActions": "Wys nutsbalk", - "hideToolbar": "Versteek nutsbalk", - "showToolbar": "Vou die nutsbalk uit", - "moveToolbar": "Skuif nutsbalk", - "moveToolbarHint": "Sleep om die nutsbalk te herposisioneer", - "layout": "Nutsbalk-vertoonmodus", - "layoutIcon": "Slegs ikoon", - "layoutLabeled": "Ikone en etikette", - "layoutExpanded": "Uitgebrei met statistieke", - "image": "Beeldaksies (slegs beelde)", - "upload": "Laai beeld op", - "paste": "Plak beeld", - "detachTmux": "Ontkoppel tmux", - "detachTmuxDescription": "Ontkoppel tmux-sessie", - "imageActionFailed": "Beeldaksie het misluk", - "retry": "Probeer weer", - "dismissError": "Maak beeldfout toe", "cpu": "SVE", "memory": "Geheue", "disk": "Skyf", "statsUnavailable": "Regstreekse statistieke is nie beskikbaar nie", "tmuxDetach": "Ontkoppel tmux-sessie", - "uploadImage": "Laai 'n beeld op na die gasheer", - "uploadingImage": "Laai tans op...", - "pasteImage": "Plak beeld vanaf knipbord (slegs beelde)", "openFiles": "Maak lêers oop", "openDocker": "Maak Docker oop", "openTunnel": "Oop Tonnel", @@ -3285,31 +3136,6 @@ "timeHours": "{{count}}uur gelede", "timeDays": "{{count}}dae gelede" }, - "mobileKeyboard": { - "shift": "Skof", - "ctrl": "Ctrl", - "esc": "Esc", - "tab": "Oortjie", - "backTab": "⇥", - "arrowUp": "Pyltjie Op", - "arrowDown": "Pyltjie Af", - "arrowLeft": "Pyltjie links", - "arrowRight": "Pyltjie Regs", - "home": "Tuis", - "end": "Einde", - "pageUp": "Bladsy Op", - "pageDown": "PgDn", - "delete": "Del", - "paste": "Plak", - "editQuickKeys": "Wysig vinnige sleutels", - "quickKeysTitle": "Vinnige sleutels", - "quickKeysDesc": "Tik × om te verwyder. Ondersteun tot 8 karakters.", - "quickKeyPlaceholder": "bv. sudo ", - "addQuickKey": "Voeg by", - "removeQuickKey": "Verwyder", - "resetDefaults": "Stel terug na verstekwaardes", - "done": "Klaar" - }, "serial": { "title": "Reeks", "portLabel": "Hawe", diff --git a/src/ui/locales/translated/ar_SA.json b/src/ui/locales/translated/ar_SA.json index 1ecdb1305..d814216a7 100644 --- a/src/ui/locales/translated/ar_SA.json +++ b/src/ui/locales/translated/ar_SA.json @@ -742,7 +742,6 @@ "addHost": "إضافة مضيف", "editHost": "تحرير المضيف", "cloneHost": "استنساخ المضيف", - "enableTerminal": "تمكين الطرفية", "enableTunnel": "تمكين النفق", "enableFileManager": "تمكين مدير الملفات", "enableDocker": "تمكين Docker", @@ -1041,8 +1040,6 @@ "enableSessionLoggingDesc": "تسجيل مخرجات الجلسة الطرفية للمراجعة لاحقاً", "allowSessionSharing": "السماح بمشاركة الجلسة", "allowSessionSharingDesc": "يمكن مشاركة الجلسات المباشرة على هذا المضيف عبر رابط أو مع مستخدمين آخرين", - "enableCommandHistory": "سجل الأوامر", - "enableCommandHistoryDesc": "تسجيل الأوامر المنفذة في هذه الطرفية للسجل والإكمال التلقائي", "localEchoLabel": "صدى محلي", "localEchoDesc": "توقع ضغطات المفاتيح الآمنة القابلة للطباعة محليًا على الاتصالات البطيئة. لا يتم عرض مطالبات كلمة المرور أو مدخلات التحكم على الشاشة.", "localEchoDefault": "استخدم الإعدادات العامة", @@ -1072,8 +1069,6 @@ "bindHost": "مضيف الربط", "enableTmuxMonitor": "تمكين مراقب Tmux", "enableTmuxMonitorDesc": "إظهار هذا المضيف في مراقب Tmux وإضافة إجراءات tmux الخاصة به إلى الشريط الجانبي", - "enableTerminalToolbar": "تفعيل شريط أدوات الطرفية", - "enableTerminalToolbarDesc": "اعرض شريط أدوات سريع الوصول مثبتًا على الطرفية النشطة لأدوات هذا المضيف وإحصائياته المباشرة.", "connectionToolbar": "شريط أدوات الاتصال", "showConnectionToolbar": "إظهار شريط أدوات الاتصال", "showConnectionToolbarDesc": "يمكنك إظهار شريط الأدوات في جلسات SSH وRDP وVNC وTelnet الخاصة بهذا المضيف. كما يمكنك إخفاؤه مؤقتًا من شريط الأدوات نفسه.", @@ -1550,9 +1545,7 @@ "links": "روابط", "navigate": "تنقل", "select": "تحديد", - "toggleWith": "تبديل باستخدام", - "localTerminal": "المحطة المحلية", - "localTerminalDesc": "افتح نافذة طرفية على هذا الكمبيوتر" + "toggleWith": "تبديل باستخدام" }, "splitScreen": { "paneEmpty": "الجزء {{index}} - فارغ", @@ -1717,57 +1710,13 @@ "loadSharesFailed": "فشل تحميل المشاركات النشطة" }, "terminal": { - "sessionExpiredNotice": "انتهت صلاحية جلستك السابقة على {{host}} بعد {{minutes}} دقيقة من قطع الاتصال؛ تم إغلاق نافذة الأوامر وأي شيء يعمل بداخلها. فعّل خاصية Auto-Tmux على هذا المضيف للحفاظ على استمرارية الجلسات.", - "sessionExpiredNoticeNoMinutes": "انتهت صلاحية جلستك السابقة على {{host}} أثناء انقطاع الاتصال؛ تم حذف واجهة سطر الأوامر وأي شيء كان يعمل بداخلها. فعّل خاصية Auto-Tmux على هذا المضيف للحفاظ على استمرارية الجلسات.", - "enableAutoTmuxAction": "تفعيل خاصية Auto-Tmux", - "autoTmuxEnabled": "تم تفعيل خاصية Auto-Tmux لـ {{host}}. وسيتم تطبيقها بدءًا من الاتصال التالي.", - "autoTmuxEnableFailed": "فشل تفعيل خاصية Auto-Tmux", "connect": "الاتصال بالمضيف", "clear": "مسح", "paste": "لصق", - "reconnect": "إعادة الاتصال", - "connectionLost": "انقطع الاتصال", "connected": "متصل", "clipboardWriteFailed": "فشل النسخ إلى الحافظة. تأكد من تقديم الصفحة عبر HTTPS أو localhost.", "clipboardReadFailed": "فشلت القراءة من الحافظة. تأكد من منح أذونات الحافظة.", - "clipboardHttpWarning": "تعذر قراءة الحافظة. بعض المتصفحات تقيّد الوصول إلى الحافظة خارج بروتوكول HTTPS - جرّب Ctrl+V بدلاً من ذلك، أو شغّل Termix عبر HTTPS.", - "passwordPromptFillTitle": "هل تريد ملء كلمة المرور المحفوظة في هذه النافذة؟", - "unknownError": "حدث خطأ غير معروف", - "websocketError": "خطأ في اتصال WebSocket", - "connecting": "جارٍ الاتصال...", - "noHostSelected": "لم يتم تحديد مضيف", - "reconnecting": "جارٍ إعادة الاتصال... ({{attempt}}/{{max}})", - "reconnected": "تمت إعادة الاتصال بنجاح", - "tmuxSessionCreated": "تم إنشاء جلسة tmux: {{name}}", - "tmuxSessionAttached": "تم الالتحاق بجلسة tmux: {{name}}", - "tmuxUnavailable": "tmux غير مثبّت على المضيف البعيد، جارٍ الرجوع إلى الصدفة القياسية", - "tmuxSessionPickerTitle": "جلسات tmux", - "tmuxSessionPickerDesc": "تم العثور على جلسات tmux موجودة على هذا المضيف. اختر واحدة لإعادة الاتصال أو أنشئ جلسة جديدة.", - "tmuxWindows": "النوافذ", - "tmuxWindowCount": "{{count}} نافذة", - "tmuxAttached": "العملاء المتصلون", - "tmuxAttachedCount": "{{count}} متصل", - "tmuxLastActivity": "آخر نشاط", - "tmuxTimeJustNow": "الآن فقط", - "tmuxTimeMinutes": "منذ {{count}}د", - "tmuxTimeHours": "منذ {{count}}س", - "tmuxTimeDays": "منذ {{count}}ي", - "tmuxCreateNew": "بدء جلسة جديدة", - "tmuxCopyHint": "اضبط التحديد واضغط Enter للنسخ إلى الحافظة", "tmuxDetach": "فصل من جلسة tmux", - "tmuxDetached": "تم الفصل من جلسة tmux", - "searchPlaceholder": "يجد", - "searchCaseSensitive": "علبة كبريت", - "searchWholeWord": "تطابق الكلمة الكاملة", - "searchRegex": "استخدم التعبير النمطي", - "searchNoResults": "لا توجد نتائج", - "searchResultCount": "{{index}} من {{count}}", - "searchNext": "المباراة التالية (إدخال)", - "searchPrevious": "المباراة السابقة (Shift+Enter)", - "searchClose": "إغلاق (الهروب)", - "maxReconnectAttemptsReached": "تم الوصول إلى الحد الأقصى لمحاولات إعادة الاتصال", - "closeTab": "إغلاق", - "connectionTimeout": "انتهت مهلة الاتصال", "terminalTitle": "الطرفية - {{host}}", "terminalWithPath": "الطرفية - {{host}}:{{path}}", "runTitle": "تشغيل {{command}} - {{host}}", @@ -1790,27 +1739,14 @@ "opksshOpenBrowser": "افتح المتصفح للمصادقة", "opksshWaitingForAuth": "في انتظار المصادقة في المتصفح...", "opksshAuthenticating": "جارٍ معالجة المصادقة...", - "opksshTimeout": "انتهت مهلة المصادقة. يرجى المحاولة مرة أخرى.", - "opksshAuthFailed": "فشلت المصادقة. يرجى التحقق من بيانات الاعتماد والمحاولة مرة أخرى.", "opksshSignInWith": "تسجيل الدخول باستخدام {{provider}}", - "vaultAuthTitle": "مطلوب تسجيل الدخول إلى Vault", - "vaultAuthDescription": "تم فتح نافذة لتسجيل الدخول إلى HashiCorp Vault. أكمل تسجيل الدخول هناك؛ سيستمر هذا الاتصال تلقائيًا.", - "vaultAuthFailed": "فشلت مصادقة Vault. يرجى المحاولة مرة أخرى.", - "vaultReopen": "أعد فتح نافذة تسجيل الدخول", "sudoPasswordPopupTitle": "إدراج كلمة المرور؟", - "linkDialogTitle": "فتح الرابط", - "linkDialogOpen": "فتح", - "linkDialogCopy": "نسخ", - "websocketAbnormalClose": "أُغلق الاتصال بشكل غير متوقع. قد يكون السبب مشكلة في الوكيل العكسي أو إعدادات SSL. يرجى مراجعة سجلات الخادم.", "connectionLogTitle": "سجل الاتصال", "connectionLogCopy": "نسخ السجلات إلى الحافظة", "connectionLogEmpty": "لا توجد سجلات اتصال بعد", "connectionLogWaiting": "في انتظار سجلات الاتصال...", "connectionLogCopied": "تم نسخ سجلات الاتصال إلى الحافظة", "connectionLogCopyFailed": "فشل نسخ السجلات إلى الحافظة", - "connectionRejected": "تم رفض الاتصال من قبل الخادم. يرجى التحقق من المصادقة وإعدادات الشبكة.", - "hostKeyRejected": "تم رفض التحقق من مفتاح مضيف SSH. تم إلغاء الاتصال.", - "sessionTakenOver": "تم فتح الجلسة في علامة تبويب أخرى. جارٍ إعادة الاتصال...", "split": { "splitTab": "تقسيم علامة التبويب", "addToSplit": "إضافة إلى التقسيم", @@ -2225,55 +2161,6 @@ "sectionApiKeys": "مفاتيح API", "sectionAuditLog": "سجل التدقيق", "sectionSsl": "SSL / Let's Encrypt", - "sectionTouchInput": "إدخال اللمس", - "touchEnabled": "تفعيل إدخال اللمس", - "touchEnabledDesc": "السماح بالتمرير بلمسة إصبع واحدة في المحطات الطرفية", - "touchMomentumEnabled": "تفعيل الزخم", - "touchMomentumEnabledDesc": "استمر في التمرير لفترة وجيزة بعد التمرير", - "touchAdvanced": "متقدم", - "touchDragThreshold": "عتبة السحب", - "touchMaxWheelDelta": "أقصى دلتا للعجلة", - "touchMomentumSampleWindow": "نافذة عينة الزخم", - "touchReleaseGracePeriod": "فترة السماح للإفراج", - "touchMinimumVelocity": "الحد الأدنى للسرعة", - "touchMaximumVelocity": "أقصى سرعة", - "touchMaximumDuration": "أقصى مدة", - "touchMaximumTravel": "أقصى قدر من السفر", - "touchDecayTime": "وقت التحلل", - "touchPixelsPerTick": "بكسل لكل نبضة", - "touchMaximumFrameInterval": "أقصى فاصل زمني بين الإطارات", - "touchMaximumTicksPerFrame": "الحد الأقصى للنبضات لكل إطار", - "touchResetDefaults": "إعادة ضبط الإعدادات الافتراضية", - "touchSaved": "تم حفظ إعدادات إدخال اللمس", - "touchSaveFailed": "فشل حفظ إعدادات إدخال اللمس", - "sectionImageStorage": "تخزين الصور", - "imageStorageMode": "وضع التخزين", - "imageStorageModeDesc": "يتم حفظ الصور الطرفية الملصقة والمرفوعة في هذا المكان. يتم تثبيت عمليات الاختيار التلقائي حسب الاتصال، سواء كان محليًا أو عن بُعد (SFTP).", - "imageStorageModeAuto": "آلي", - "imageStorageModeLocal": "محلي", - "imageStorageModeRemoteSftp": "عن بعد (SFTP)", - "imageStorageLocalDir": "الدليل المحلي", - "imageStorageLocalDirDesc": "المسار المطلق في نظام Termix الخلفي. لا يظهر بعد الحفظ لأسباب أمنية.", - "imageStorageLocalDirPlaceholder": "اتركه فارغًا للحفاظ على التحديث", - "imageStorageHostPath": "مسار المضيف", - "imageStorageHostPathDesc": "عرض الدليل المحلي من جانب المضيف الذي تتلقاه وكلاء المحطة الطرفية.", - "imageStorageTtl": "مدة البقاء (بالمللي ثانية)", - "imageStorageMaxCount": "الحد الأقصى لعدد الصور", - "imageStorageMaxBytes": "الحد الأقصى لحجم التخزين بالبايت", - "imageStorageSaved": "تم حفظ إعدادات تخزين الصور", - "imageStorageSaveFailed": "فشل حفظ إعدادات تخزين الصور", - "imageStorageInstanceId": "معرّف مثيل جلسة الطرفية", - "imageStorageInstanceIdDesc": "معرف مثيل إحدى علامات تبويب الطرفية المتصلة، المستخدمة لاختبار الرؤية.", - "imageStorageInstanceIdRequired": "أدخل أولاً معرّف مثيل جلسة طرفية", - "imageStorageTest": "امتحان", - "imageStorageTesting": "جارٍ الاختبار...", - "imageStorageTestFailed": "فشل اختبار تخزين الصور", - "imageStorageTestConnected": "تم الاتصال بجلسة الطرفية.", - "imageStorageTestNotConnected": "لا توجد جلسة طرفية متصلة بهذا المعرف.", - "imageStorageTestSelectedMode": "سيتم استخدام الأمر التالي للتحميل: {{mode}}", - "imageStorageSelectedModeUnavailable": "غير متاح", - "imageStorageTestLocalVisible": "الدليل المحلي مرئي من الجهاز المضيف المتصل.", - "imageStorageTestLocalNotVisible": "الدليل المحلي غير مرئي من الجهاز المضيف المتصل.", "sectionBranding": "العلامة البيضاء", "brandingAppName": "اسم التطبيق", "brandingAppNameDesc": "يظهر هذا الحقل في شاشة تسجيل الدخول وعلامة تبويب المتصفح. اتركه فارغًا لاستخدام \"Termix\".", @@ -2354,9 +2241,6 @@ "oidcSilentLoginDefaultLockedDesc": "هذا الإعداد مقفل بواسطة متغير البيئة OIDC_SILENT_LOGIN_DEFAULT ولا يمكن تغييره هنا.", "allowPasswordReset": "السماح بإعادة تعيين كلمة المرور", "allowPasswordResetDesc": "رمز إعادة التعيين عبر سجلات Docker", - "commandHistoryEnabled": "سجل الأوامر", - "commandHistoryEnabledDesc": "السماح بتسجيل سجل الأوامر. عند التعطيل، لا يتم حفظ السجل بغض النظر عن إعدادات كل جهاز.", - "updateCommandHistoryFailed": "فشل تحديث إعداد سجل الأوامر", "analyticsEnabled": "مشاركة إحصائيات الاستخدام المجهولة", "analyticsEnabledDesc": "يرسل النظام إحصاءً يومياً مجهولاً للمستخدمين والمضيفين واستخدام الميزات للمساعدة في تحسين Termix. ولا يتم تضمين أي بيانات شخصية أو تفاصيل اتصال على الإطلاق.", "analyticsEnabledLockedDesc": "هذا الإعداد مقفل بواسطة متغير البيئة ENABLE_TELEMETRY ولا يمكن تغييره هنا.", @@ -2647,11 +2531,6 @@ "hostDefaultsTerminal": "الطرفية", "hostDefaultsAutoTmux": "Auto-Tmux", "hostDefaultsAutoTmuxDesc": "قم بتشغيل طرفيات المضيفين الجدد داخل tmux حتى تستمر الجلسات في حالة انقطاع الاتصال (يتطلب tmux على المضيف).", - "terminalSessionTimeout": "استمرار جلسة الطرفية", - "terminalSessionTimeoutDesc": "مدة بقاء طرفية غير متصلة على الخادم قبل إغلاقها. الحد الأدنى: دقيقة واحدة، الحد الأقصى: 1440 دقيقة. بالنسبة للجلسات التي يجب أن تستمر لفترة أطول من ذلك، فعّل خاصية Auto-Tmux على المضيف.", - "terminalSessionTimeoutRange": "أدخل قيمة بين 1 و 1440 دقيقة", - "terminalSessionTimeoutSaved": "تم حفظ استمرارية جلسة الطرفية", - "terminalSessionTimeoutSaveFailed": "فشل حفظ بيانات جلسة الطرفية", "minutes": "دقائق", "hostDefaultsSessionLogging": "تسجيل الجلسات", "hostDefaultsSessionLoggingDesc": "تسجيل جلسات الطرفية على المضيفين الجدد افتراضيًا", @@ -2708,15 +2587,6 @@ "connectToTerminal": "اتصل بالطرفية", "connectToFiles": "اتصل بالملفات" }, - "history": { - "noTerminalSelected": "لا توجد طرفية محددة", - "noTerminalSelectedHint": "افتح علامة تبويب طرفية SSH لعرض سجل الأوامر الخاص بها", - "searchPlaceholder": "ابحث في السجل...", - "clearAll": "مسح الكل", - "noHistoryEntries": "لا توجد مدخلات سجل", - "trackingDisabled": "تتبع السجل معطل", - "trackingDisabledHint": "قم بتمكينه في إعدادات الطرفية الخاصة بالمضيف." - }, "sshTools": { "keyRecordingTitle": "تسجيل المفاتيح", "recordToTerminals": "التسجيل إلى الطرفيات", @@ -3178,30 +3048,11 @@ "terminalToolbar": { "title": "شريط أدوات الطرفية", "showActions": "إظهار شريط الأدوات", - "hideToolbar": "إخفاء شريط الأدوات", - "showToolbar": "توسيع شريط الأدوات", - "moveToolbar": "نقل شريط الأدوات", - "moveToolbarHint": "اسحب لإعادة وضع شريط الأدوات", - "layout": "وضع عرض شريط الأدوات", - "layoutIcon": "أيقونة فقط", - "layoutLabeled": "الرموز والملصقات", - "layoutExpanded": "تم توسيعه بمقاييس", - "image": "إجراءات الصور (للصور فقط)", - "upload": "تحميل صورة", - "paste": "لصق الصورة", - "detachTmux": "افصل tmux", - "detachTmuxDescription": "فصل جلسة tmux", - "imageActionFailed": "فشلت عملية معالجة الصورة", - "retry": "إعادة المحاولة", - "dismissError": "تجاهل خطأ الصورة", "cpu": "وحدة المعالجة المركزية", "memory": "ذاكرة", "disk": "أدك", "statsUnavailable": "الإحصائيات المباشرة غير متوفرة", "tmuxDetach": "فصل جلسة tmux", - "uploadImage": "قم بتحميل صورة إلى المضيف", - "uploadingImage": "جارٍ التحميل...", - "pasteImage": "لصق الصورة من الحافظة (الصور فقط)", "openFiles": "فتح الملفات", "openDocker": "أوبن دوكر", "openTunnel": "نفق مفتوح", @@ -3285,31 +3136,6 @@ "timeHours": "قبل {{count}}h", "timeDays": "قبل {{count}}d" }, - "mobileKeyboard": { - "shift": "Shift", - "ctrl": "Ctrl", - "esc": "Esc", - "tab": "Tab", - "backTab": "⇥", - "arrowUp": "Arrow Up", - "arrowDown": "Arrow Down", - "arrowLeft": "Arrow Left", - "arrowRight": "Arrow Right", - "home": "Home", - "end": "End", - "pageUp": "PgUp", - "pageDown": "PgDn", - "delete": "Del", - "paste": "لصق", - "editQuickKeys": "تحرير المفاتيح السريعة", - "quickKeysTitle": "المفاتيح السريعة", - "quickKeysDesc": "اضغط × للإزالة. يدعم حتى 8 أحرف.", - "quickKeyPlaceholder": "مثال: sudo ", - "addQuickKey": "إضافة", - "removeQuickKey": "إزالة", - "resetDefaults": "استعادة الإعدادات الافتراضية", - "done": "تم" - }, "serial": { "title": "منفذ تسلسلي", "portLabel": "المنفذ", diff --git a/src/ui/locales/translated/bg_BG.json b/src/ui/locales/translated/bg_BG.json index 811b2c40c..919e331a8 100644 --- a/src/ui/locales/translated/bg_BG.json +++ b/src/ui/locales/translated/bg_BG.json @@ -742,7 +742,6 @@ "addHost": "Добавяне на хост", "editHost": "Редактиране на хост", "cloneHost": "Клониране на хост", - "enableTerminal": "Активиране на терминала", "enableTunnel": "Активиране на тунел", "enableFileManager": "Активиране на файловия мениджър", "enableDocker": "Активиране на Докер", @@ -1041,8 +1040,6 @@ "enableSessionLoggingDesc": "Записване на изхода от терминалната сесия за по-късен преглед", "allowSessionSharing": "Разрешаване на споделяне на сесия", "allowSessionSharingDesc": "Позволете споделянето на сесиите на живо на този хост чрез линк или с други потребители", - "enableCommandHistory": "История на командите", - "enableCommandHistoryDesc": "Запис на команди, изпълнявани в този терминал, за история и автоматично довършване", "localEchoLabel": "Локално ехо", "localEchoDesc": "Предсказвайте безопасни за печат натискания на клавиши локално при бавни връзки. Подканите за парола и контролните въведени команди никога не се повтарят.", "localEchoDefault": "Използвайте глобална настройка", @@ -1072,8 +1069,6 @@ "bindHost": "Свързване на хост", "enableTmuxMonitor": "Активиране на Tmux монитор", "enableTmuxMonitorDesc": "Показване на този хост в Tmux монитора и добавяне на неговите tmux действия към страничната лента", - "enableTerminalToolbar": "Активиране на лентата с инструменти на терминала", - "enableTerminalToolbarDesc": "Показване на лента с инструменти за бърз достъп, закачена към активния терминал, за инструментите и статистиката в реално време на този хост", "connectionToolbar": "Инструменти за свързване", "showConnectionToolbar": "Показване на лентата с инструменти за връзки", "showConnectionToolbarDesc": "Показване на лентата с инструменти в SSH, RDP, VNC и Telnet сесиите на този хост. Можете също така временно да я скриете от самата лента с инструменти.", @@ -1550,9 +1545,7 @@ "links": "Връзки", "navigate": "Навигиране", "select": "Изберете", - "toggleWith": "Превключване с", - "localTerminal": "Местен терминал", - "localTerminalDesc": "Отворете обвивка на този компютър" + "toggleWith": "Превключване с" }, "splitScreen": { "paneEmpty": "Панел {{index}} - празен", @@ -1717,57 +1710,13 @@ "loadSharesFailed": "Зареждането на активните споделяния не бе успешно" }, "terminal": { - "sessionExpiredNotice": "Предишната ви сесия на {{host}} изтече след {{minutes}} минути прекъснато свързване; обвивката и всичко, което се изпълнява в нея, са изчезнали. Активирайте Auto-Tmux на този хост, за да поддържате сесиите активни.", - "sessionExpiredNoticeNoMinutes": "Предишната ви сесия на {{host}} изтече, докато бяхте прекъснати; обвивката и всичко, което се изпълняваше в нея, са изчезнали. Активирайте Auto-Tmux на този хост, за да поддържате сесиите активни.", - "enableAutoTmuxAction": "Активиране на Auto-Tmux", - "autoTmuxEnabled": "Автоматичното Tmux управление е активирано за {{host}}. Прилага се от следващата връзка.", - "autoTmuxEnableFailed": "Неуспешно активиране на Auto-Tmux", "connect": "Свързване с хоста", "clear": "Изчисти", "paste": "Паста", - "reconnect": "Свържете се отново", - "connectionLost": "Връзката е прекъсната", "connected": "Свързан", "clipboardWriteFailed": "Копирането в буфера не бе успешно. Уверете се, че страницата се предоставя през HTTPS или localhost.", "clipboardReadFailed": "Четенето от клипборда не бе успешно. Уверете се, че са предоставени разрешения за достъп до клипборда.", - "clipboardHttpWarning": "Не можах да прочета клипборда. Някои браузъри ограничават достъпа до клипборда извън HTTPS - опитайте Ctrl+V вместо това или използвайте Termix през HTTPS.", - "passwordPromptFillTitle": "Да се попълни ли запазената парола в този ред?", - "unknownError": "Възникна неизвестна грешка", - "websocketError": "Грешка при свързване с WebSocket", - "connecting": "Свързване...", - "noHostSelected": "Няма избран хост", - "reconnecting": "Повторно свързване... ({{attempt}}/{{max}})", - "reconnected": "Успешно повторно свързване", - "tmuxSessionCreated": "tmux сесия, създадена: {{name}}", - "tmuxSessionAttached": "Прикачена tmux сесия: {{name}}", - "tmuxUnavailable": "tmux не е инсталиран на отдалечения хост, връща се към стандартната обвивка", - "tmuxSessionPickerTitle": "tmux сесии", - "tmuxSessionPickerDesc": "На този хост са открити съществуващи tmux сесии. Изберете една, за да я свържете отново или да създадете нова сесия.", - "tmuxWindows": "Прозорци", - "tmuxWindowCount": "{{count}} прозорец", - "tmuxAttached": "Прикачени клиенти", - "tmuxAttachedCount": "{{count}} прикачен", - "tmuxLastActivity": "Последна активност", - "tmuxTimeJustNow": "точно сега", - "tmuxTimeMinutes": "преди {{count}}мин.", - "tmuxTimeHours": "преди {{count}}ч", - "tmuxTimeDays": "преди {{count}}дни", - "tmuxCreateNew": "Започнете нова сесия", - "tmuxCopyHint": "Променете селекцията и натиснете Enter, за да я копирате в клипборда", "tmuxDetach": "Отделяне от tmux сесия", - "tmuxDetached": "Отделен от tmux сесията", - "searchPlaceholder": "Намерете", - "searchCaseSensitive": "Съвпадение на регистъра", - "searchWholeWord": "Съвпадение на цяла дума", - "searchRegex": "Използвайте регулярни изрази", - "searchNoResults": "Няма резултати", - "searchResultCount": "{{index}} от {{count}}", - "searchNext": "Следващ мач (Enter)", - "searchPrevious": "Предишно съвпадение (Shift+Enter)", - "searchClose": "Затвори (Escape)", - "maxReconnectAttemptsReached": "Достигнат е максималният брой опити за повторно свързване", - "closeTab": "Затвори", - "connectionTimeout": "Време за изчакване на връзката", "terminalTitle": "Терминал - {{host}}", "terminalWithPath": "Терминал - {{host}}:{{path}}", "runTitle": "Изпълнява се {{command}} - {{host}}", @@ -1790,27 +1739,14 @@ "opksshOpenBrowser": "Отворете браузъра за удостоверяване", "opksshWaitingForAuth": "Чака се удостоверяване в браузъра...", "opksshAuthenticating": "Обработва се удостоверяване...", - "opksshTimeout": "Времето за изчакване на удостоверяването изтече. Моля, опитайте отново.", - "opksshAuthFailed": "Удостоверяването не бе успешно. Моля, проверете идентификационните си данни и опитайте отново.", "opksshSignInWith": "Влезте с {{provider}}", - "vaultAuthTitle": "Изисква се влизане в трезора", - "vaultAuthDescription": "Отвори се прозорец за влизане в HashiCorp Vault. Завършете влизането там; тази връзка ще продължи автоматично.", - "vaultAuthFailed": "Удостоверяването на трезора не бе успешно. Моля, опитайте отново.", - "vaultReopen": "Отваряне отново на прозореца за влизане", "sudoPasswordPopupTitle": "Въвеждане на парола?", - "linkDialogTitle": "Отвори линка", - "linkDialogOpen": "Отворено", - "linkDialogCopy": "Копиране", - "websocketAbnormalClose": "Връзката се прекъсна неочаквано. Това може да се дължи на проблем с обратен прокси или SSL конфигурация. Моля, проверете лог файловете на сървъра.", "connectionLogTitle": "Дневник на връзките", "connectionLogCopy": "Копиране на лог файлове в клипборда", "connectionLogEmpty": "Все още няма регистрационни файлове за връзка", "connectionLogWaiting": "Чакат се регистрационни файлове за връзка...", "connectionLogCopied": "Дневниците на връзките са копирани в клипборда", "connectionLogCopyFailed": "Копирането на регистрационните файлове в буфера не бе успешно", - "connectionRejected": "Връзката е отхвърлена от сървъра. Моля, проверете удостоверяването и мрежовата си конфигурация.", - "hostKeyRejected": "Проверката на SSH ключа за хост е отхвърлена. Връзката е прекратена.", - "sessionTakenOver": "Сесията беше отворена в друг раздел. Повторно свързване...", "split": { "splitTab": "Разделяне на раздела", "addToSplit": "Добави към Сплит", @@ -2225,55 +2161,6 @@ "sectionApiKeys": "API ключове", "sectionAuditLog": "Дневник на одита", "sectionSsl": "SSL / Нека криптираме", - "sectionTouchInput": "Сензорно въвеждане", - "touchEnabled": "Активиране на сензорно въвеждане", - "touchEnabledDesc": "Разрешаване на превъртане с един пръст в терминали", - "touchMomentumEnabled": "Активиране на Momentum", - "touchMomentumEnabledDesc": "Продължете да превъртате за кратко след плъзване", - "touchAdvanced": "Разширено", - "touchDragThreshold": "Праг на плъзгане", - "touchMaxWheelDelta": "Максимална делта на колелото", - "touchMomentumSampleWindow": "Прозорец за пример на импулса", - "touchReleaseGracePeriod": "Гратисен период за освобождаване", - "touchMinimumVelocity": "Минимална скорост", - "touchMaximumVelocity": "Максимална скорост", - "touchMaximumDuration": "Максимална продължителност", - "touchMaximumTravel": "Максимален ход", - "touchDecayTime": "Време на разпад", - "touchPixelsPerTick": "Пиксели на такт", - "touchMaximumFrameInterval": "Максимален интервал на кадрите", - "touchMaximumTicksPerFrame": "Максимален брой отметки на кадър", - "touchResetDefaults": "Възстановяване на фабричните настройки", - "touchSaved": "Настройките за въвеждане с докосване са запазени", - "touchSaveFailed": "Запазването на настройките за сензорно въвеждане не бе успешно", - "sectionImageStorage": "Съхранение на изображения", - "imageStorageMode": "Режим на съхранение", - "imageStorageModeDesc": "Където се записват поставените и качени изображения на терминала. Автоматичното избиране по връзка, локално и отдалечено (SFTP) е поправено.", - "imageStorageModeAuto": "Автоматично", - "imageStorageModeLocal": "Местно", - "imageStorageModeRemoteSftp": "Отдалечено (SFTP)", - "imageStorageLocalDir": "Локален указател", - "imageStorageLocalDirDesc": "Абсолютен път в сървъра на Termix. Не се показва след запазване от съображения за сигурност.", - "imageStorageLocalDirPlaceholder": "Оставете празно, за да остане актуално", - "imageStorageHostPath": "Път на хоста", - "imageStorageHostPathDesc": "Изгледът от страна на хоста на локалната директория, която получават терминалните агенти.", - "imageStorageTtl": "TTL (мсек)", - "imageStorageMaxCount": "Максимален брой изображения", - "imageStorageMaxBytes": "Максимален брой байтове за съхранение", - "imageStorageSaved": "Настройките за съхранение на изображения са запазени", - "imageStorageSaveFailed": "Запазването на настройките за съхранение на изображения не бе успешно", - "imageStorageInstanceId": "Идентификатор на екземпляр на терминална сесия", - "imageStorageInstanceIdDesc": "Идентификатор на екземпляр на един от свързаните ви терминални раздели, използван за тестване на видимостта.", - "imageStorageInstanceIdRequired": "Първо въведете идентификатор на екземпляр на терминална сесия", - "imageStorageTest": "Тест", - "imageStorageTesting": "Тестване...", - "imageStorageTestFailed": "Тестването на съхранението на изображения не бе успешно", - "imageStorageTestConnected": "Терминалната сесия е свързана.", - "imageStorageTestNotConnected": "Няма свързана терминална сесия с този идентификатор на екземпляр.", - "imageStorageTestSelectedMode": "Качването ще използва: {{mode}}", - "imageStorageSelectedModeUnavailable": "Не е налично", - "imageStorageTestLocalVisible": "Локалната директория е видима от свързания хост.", - "imageStorageTestLocalNotVisible": "Локалната директория НЕ е видима от свързания хост.", "sectionBranding": "Бял етикет", "brandingAppName": "Име на приложението", "brandingAppNameDesc": "Показва се на екрана за вход и в раздела на браузъра. Оставете празно, за да използвате „Termix“.", @@ -2354,9 +2241,6 @@ "oidcSilentLoginDefaultLockedDesc": "Тази настройка е заключена от променливата на средата OIDC_SILENT_LOGIN_DEFAULT и не може да бъде променена тук.", "allowPasswordReset": "Разрешаване на нулиране на паролата", "allowPasswordResetDesc": "Нулиране на кода чрез Docker логове", - "commandHistoryEnabled": "История на командите", - "commandHistoryEnabledDesc": "Разрешаване на записване на историята на командите. Когато е деактивирано, историята не се запазва, независимо от настройките за всеки хост.", - "updateCommandHistoryFailed": "Актуализирането на настройката за историята на командите не бе успешно", "analyticsEnabled": "Споделяне на анонимни статистически данни за употреба", "analyticsEnabledDesc": "Изпраща анонимен дневен брой потребители, хостове и използване на функции, за да помогне за подобряването на Termix. Никога не се включват лични данни или подробности за връзката.", "analyticsEnabledLockedDesc": "Тази настройка е заключена от променливата на средата ENABLE_TELEMETRY и не може да бъде променена тук.", @@ -2647,11 +2531,6 @@ "hostDefaultsTerminal": "Терминал", "hostDefaultsAutoTmux": "Auto-Tmux", "hostDefaultsAutoTmuxDesc": "Стартирайте терминалите на новите хостове вътре в tmux, така че сесиите да оцелеят при прекъсвания (необходим е tmux на хоста)", - "terminalSessionTimeout": "Устойчивост на терминална сесия", - "terminalSessionTimeoutDesc": "Колко дълго един прекъснат терминал се поддържа активен на сървъра, преди да бъде затворен. Мин. 1 · Макс. 1440 минути. За сесии, които трябва да преживеят това, активирайте Auto-Tmux на хоста.", - "terminalSessionTimeoutRange": "Въведете стойност между 1 и 1440 минути", - "terminalSessionTimeoutSaved": "Запазено е запазване на трайността на терминалната сесия", - "terminalSessionTimeoutSaveFailed": "Неуспешно запазване на постоянството на терминалната сесия", "minutes": "минути", "hostDefaultsSessionLogging": "Записване на сесии", "hostDefaultsSessionLoggingDesc": "Записване на терминални сесии на нови хостове по подразбиране", @@ -2708,15 +2587,6 @@ "connectToTerminal": "Свързване с терминала", "connectToFiles": "Свързване с файлове" }, - "history": { - "noTerminalSelected": "Няма избран терминал", - "noTerminalSelectedHint": "Отворете раздел на SSH терминал, за да видите историята на командите му", - "searchPlaceholder": "История на търсенията...", - "clearAll": "Изчисти всички", - "noHistoryEntries": "Няма записи в историята", - "trackingDisabled": "Проследяването на историята е деактивирано", - "trackingDisabledHint": "Активирайте го в настройките на терминала на хоста." - }, "sshTools": { "keyRecordingTitle": "Запис на ключове", "recordToTerminals": "Записване към терминали", @@ -3178,30 +3048,11 @@ "terminalToolbar": { "title": "Инструменти с инструменти на терминала", "showActions": "Показване на лентата с инструменти", - "hideToolbar": "Скриване на лентата с инструменти", - "showToolbar": "Разгъване на лентата с инструменти", - "moveToolbar": "Преместване на лентата с инструменти", - "moveToolbarHint": "Плъзнете, за да препозиционирате лентата с инструменти", - "layout": "Режим на показване на лентата с инструменти", - "layoutIcon": "Само икона", - "layoutLabeled": "Икони и етикети", - "layoutExpanded": "Разширено с показатели", - "image": "Действия с изображения (само с изображения)", - "upload": "Качване на изображение", - "paste": "Поставяне на изображение", - "detachTmux": "Отделяне на tmux", - "detachTmuxDescription": "Отделяне на tmux сесия", - "imageActionFailed": "Действието с изображението не беше успешно", - "retry": "Опитай отново", - "dismissError": "Грешка при отхвърляне на изображението", "cpu": "Процесор", "memory": "Памет", "disk": "Диск", "statsUnavailable": "Статистически данни в реално време не са налични", "tmuxDetach": "Отделяне на tmux сесия", - "uploadImage": "Качете изображение на хоста", - "uploadingImage": "Качване...", - "pasteImage": "Поставяне на изображение от клипборда (само за изображения)", "openFiles": "Отворени файлове", "openDocker": "Отвори Докер", "openTunnel": "Отворен тунел", @@ -3285,31 +3136,6 @@ "timeHours": "преди {{count}}ч", "timeDays": "преди {{count}}дни" }, - "mobileKeyboard": { - "shift": "Изместване", - "ctrl": "Ctrl", - "esc": "Esc", - "tab": "Раздел", - "backTab": "⇥", - "arrowUp": "Стрелка нагоре", - "arrowDown": "Стрелка надолу", - "arrowLeft": "Стрелка наляво", - "arrowRight": "Стрелка надясно", - "home": "Дом", - "end": "Край", - "pageUp": "PgUp", - "pageDown": "PgDn", - "delete": "Дел", - "paste": "Паста", - "editQuickKeys": "Редактиране на бързи клавиши", - "quickKeysTitle": "Бързи клавиши", - "quickKeysDesc": "Докоснете × за премахване. Поддържа до 8 знака.", - "quickKeyPlaceholder": "например sudo ", - "addQuickKey": "Добавяне", - "removeQuickKey": "Премахване", - "resetDefaults": "Възстановяване на фабричните настройки", - "done": "Готово" - }, "serial": { "title": "Сериен", "portLabel": "Порт", diff --git a/src/ui/locales/translated/bn_BD.json b/src/ui/locales/translated/bn_BD.json index 907bb3019..13270e048 100644 --- a/src/ui/locales/translated/bn_BD.json +++ b/src/ui/locales/translated/bn_BD.json @@ -742,7 +742,6 @@ "addHost": "হোস্ট যোগ করুন", "editHost": "হোস্ট সম্পাদনা করুন", "cloneHost": "ক্লোন হোস্ট", - "enableTerminal": "টার্মিনাল সক্রিয় করুন", "enableTunnel": "টানেল সক্রিয় করুন", "enableFileManager": "ফাইল ম্যানেজার সক্রিয় করুন", "enableDocker": "ডকার সক্রিয় করুন", @@ -1041,8 +1040,6 @@ "enableSessionLoggingDesc": "পরবর্তী পর্যালোচনার জন্য টার্মিনাল সেশনের আউটপুট রেকর্ড করুন।", "allowSessionSharing": "সেশন শেয়ারিং অনুমোদন করুন", "allowSessionSharingDesc": "এই হোস্টের লাইভ সেশনগুলো লিঙ্কের মাধ্যমে বা অন্যান্য ব্যবহারকারীদের সাথে শেয়ার করা যাবে।", - "enableCommandHistory": "কমান্ডের ইতিহাস", - "enableCommandHistoryDesc": "ইতিহাস এবং স্বয়ংক্রিয় সম্পূর্ণতার জন্য এই টার্মিনালে চালানো কমান্ডগুলো রেকর্ড করুন।", "localEchoLabel": "স্থানীয় প্রতিধ্বনি", "localEchoDesc": "ধীরগতির সংযোগে স্থানীয়ভাবে নিরাপদ ও মুদ্রণযোগ্য কীস্ট্রোক অনুমান করুন। পাসওয়ার্ড প্রম্পট এবং কন্ট্রোল ইনপুট কখনও প্রতিধ্বনিত হয় না।", "localEchoDefault": "গ্লোবাল সেটিং ব্যবহার করুন", @@ -1072,8 +1069,6 @@ "bindHost": "হোস্টকে বাঁধুন", "enableTmuxMonitor": "Tmux মনিটর সক্রিয় করুন", "enableTmuxMonitorDesc": "এই হোস্টটিকে Tmux মনিটরে দেখান এবং এর tmux অ্যাকশনগুলো সাইডবারে যোগ করুন।", - "enableTerminalToolbar": "টার্মিনাল টুলবার সক্রিয় করুন", - "enableTerminalToolbarDesc": "এই হোস্টের টুলস এবং লাইভ পরিসংখ্যানের জন্য সক্রিয় টার্মিনালে ডক করা একটি কুইক-অ্যাক্সেস টুলবার দেখান।", "connectionToolbar": "সংযোগ টুলবার", "showConnectionToolbar": "সংযোগ টুলবার দেখান", "showConnectionToolbarDesc": "এই হোস্টের SSH, RDP, VNC, এবং Telnet সেশনগুলিতে টুলবারটি দেখান। আপনি টুলবারটি থেকেই এটিকে সাময়িকভাবে লুকাতেও পারেন।", @@ -1550,9 +1545,7 @@ "links": "লিঙ্ক", "navigate": "নেভিগেট করুন", "select": "নির্বাচন করুন", - "toggleWith": "টগল করুন", - "localTerminal": "স্থানীয় টার্মিনাল", - "localTerminalDesc": "এই কম্পিউটারে একটি শেল খুলুন" + "toggleWith": "টগল করুন" }, "splitScreen": { "paneEmpty": "প্যান {{index}} - খালি", @@ -1717,57 +1710,13 @@ "loadSharesFailed": "সক্রিয় শেয়ারগুলি লোড করতে ব্যর্থ হয়েছে" }, "terminal": { - "sessionExpiredNotice": "{{host}} -এ আপনার পূর্ববর্তী সেশনটি {{minutes}} মিনিট পর সংযোগ বিচ্ছিন্ন হয়ে মেয়াদোত্তীর্ণ হয়েছে; শেল এবং এর মধ্যে চলমান সবকিছু চলে গেছে। সেশনগুলো সচল রাখতে এই হোস্টে অটো-টিমাক্স (Auto-Tmux) সক্রিয় করুন।", - "sessionExpiredNoticeNoMinutes": "{{host}} -এ আপনার পূর্ববর্তী সেশনটি সংযোগ বিচ্ছিন্ন থাকাকালীন মেয়াদোত্তীর্ণ হয়ে গেছে; শেল এবং এর মধ্যে চলমান সবকিছু মুছে গেছে। সেশনগুলো সচল রাখতে এই হোস্টে অটো-টিমাক্স (Auto-Tmux) সক্রিয় করুন।", - "enableAutoTmuxAction": "অটো-Tmux সক্রিয় করুন", - "autoTmuxEnabled": "{{host}}এর জন্য অটো-টিমাক্স সক্রিয় করা হয়েছে। এটি পরবর্তী সংযোগ থেকে প্রযোজ্য হবে।", - "autoTmuxEnableFailed": "অটো-টিমাক্স সক্রিয় করতে ব্যর্থ হয়েছে", "connect": "হোস্টের সাথে সংযোগ করুন", "clear": "পরিষ্কার", "paste": "পেস্ট", - "reconnect": "পুনরায় সংযোগ করুন", - "connectionLost": "সংযোগ বিচ্ছিন্ন", "connected": "সংযুক্ত", "clipboardWriteFailed": "ক্লিপবোর্ডে কপি করা সম্ভব হয়নি। নিশ্চিত করুন যে পৃষ্ঠাটি HTTPS বা লোকালহোস্টের মাধ্যমে পরিবেশিত হচ্ছে।", "clipboardReadFailed": "ক্লিপবোর্ড থেকে পড়তে ব্যর্থ হয়েছে। নিশ্চিত করুন যে ক্লিপবোর্ডের অনুমতি দেওয়া আছে।", - "clipboardHttpWarning": "ক্লিপবোর্ডটি পড়া যায়নি। কিছু ব্রাউজার HTTPS-এর বাইরে ক্লিপবোর্ড অ্যাক্সেস সীমাবদ্ধ করে - এর পরিবর্তে Ctrl+V ব্যবহার করে দেখুন, অথবা HTTPS-এর মাধ্যমে Termix পরিবেশন করুন।", - "passwordPromptFillTitle": "সংরক্ষিত পাসওয়ার্ডটি এই প্রম্পটে পূরণ করুন?", - "unknownError": "অজানা ত্রুটি ঘটেছে", - "websocketError": "ওয়েবসকেট সংযোগ ত্রুটি", - "connecting": "সংযোগ স্থাপন...", - "noHostSelected": "কোন হোস্ট নির্বাচন করা হয়নি", - "reconnecting": "পুনরায় সংযোগ স্থাপন করা হচ্ছে... ({{attempt}}/{{max}})", - "reconnected": "সফলভাবে পুনরায় সংযুক্ত হয়েছে", - "tmuxSessionCreated": "tmux সেশন তৈরি হয়েছে: {{name}}", - "tmuxSessionAttached": "tmux সেশন সংযুক্ত হয়েছে: {{name}}", - "tmuxUnavailable": "রিমোট হোস্টে tmux ইনস্টল করা নেই, তাই স্ট্যান্ডার্ড শেল ব্যবহার করা হচ্ছে।", - "tmuxSessionPickerTitle": "tmux সেশন", - "tmuxSessionPickerDesc": "এই হোস্টে বিদ্যমান tmux সেশন পাওয়া গেছে। পুনরায় সংযুক্ত হতে বা একটি নতুন সেশন তৈরি করতে যেকোনো একটি নির্বাচন করুন।", - "tmuxWindows": "উইন্ডোজ", - "tmuxWindowCount": "{{count}} জানালা", - "tmuxAttached": "সংযুক্ত ক্লায়েন্ট", - "tmuxAttachedCount": "{{count}} সংযুক্ত", - "tmuxLastActivity": "শেষ কার্যকলাপ", - "tmuxTimeJustNow": "এইমাত্র", - "tmuxTimeMinutes": "{{count}}মিনিট আগে", - "tmuxTimeHours": "{{count}}ঘণ্টা আগে", - "tmuxTimeDays": "{{count}}দিন আগে", - "tmuxCreateNew": "নতুন সেশন শুরু করুন", - "tmuxCopyHint": "নির্বাচনটি ঠিক করুন এবং ক্লিপবোর্ডে কপি করতে এন্টার চাপুন।", "tmuxDetach": "tmux সেশন থেকে বিচ্ছিন্ন করুন", - "tmuxDetached": "tmux সেশন থেকে বিচ্ছিন্ন", - "searchPlaceholder": "খুঁজুন", - "searchCaseSensitive": "দেশলাইয়ের বাক্স", - "searchWholeWord": "সম্পূর্ণ শব্দ মেলান", - "searchRegex": "রেগুলার এক্সপ্রেশন ব্যবহার করুন", - "searchNoResults": "কোন ফলাফল নেই", - "searchResultCount": "{{index}} এর {{count}}", - "searchNext": "পরবর্তী ম্যাচ (প্রবেশ করুন)", - "searchPrevious": "পূর্ববর্তী ম্যাচ (Shift+Enter)", - "searchClose": "বন্ধ করুন (পলায়ন)", - "maxReconnectAttemptsReached": "পুনঃসংযোগের সর্বোচ্চ প্রচেষ্টা শেষ হয়েছে", - "closeTab": "বন্ধ করুন", - "connectionTimeout": "সংযোগের সময়সীমা শেষ", "terminalTitle": "টার্মিনাল - {{host}}", "terminalWithPath": "টার্মিনাল - {{host}}:{{path}}", "runTitle": "দৌড়ানো {{command}} - {{host}}", @@ -1790,27 +1739,14 @@ "opksshOpenBrowser": "প্রমাণীকরণের জন্য ব্রাউজার খুলুন", "opksshWaitingForAuth": "ব্রাউজারে প্রমাণীকরণের জন্য অপেক্ষা করা হচ্ছে...", "opksshAuthenticating": "প্রমাণীকরণ প্রক্রিয়া চলছে...", - "opksshTimeout": "প্রমাণীকরণের সময়সীমা শেষ হয়ে গেছে। অনুগ্রহ করে আবার চেষ্টা করুন।", - "opksshAuthFailed": "প্রমাণীকরণ ব্যর্থ হয়েছে। অনুগ্রহ করে আপনার পরিচয়পত্র যাচাই করে আবার চেষ্টা করুন।", "opksshSignInWith": "{{provider}} দিয়ে সাইন ইন করুন", - "vaultAuthTitle": "ভল্টে সাইন-ইন করা আবশ্যক", - "vaultAuthDescription": "হ্যাশিকর্প ভল্টে সাইন ইন করার জন্য একটি উইন্ডো খুলেছে। সেখানে সাইন-ইন সম্পন্ন করুন; এই সংযোগটি স্বয়ংক্রিয়ভাবে চলতে থাকবে।", - "vaultAuthFailed": "ভল্ট প্রমাণীকরণ ব্যর্থ হয়েছে। অনুগ্রহ করে আবার চেষ্টা করুন।", - "vaultReopen": "সাইন-ইন উইন্ডো পুনরায় খুলুন", "sudoPasswordPopupTitle": "পাসওয়ার্ড দিন?", - "linkDialogTitle": "লিঙ্ক খুলুন", - "linkDialogOpen": "খোলা", - "linkDialogCopy": "অনুলিপি", - "websocketAbnormalClose": "সংযোগটি অপ্রত্যাশিতভাবে বন্ধ হয়ে গেছে। এটি রিভার্স প্রক্সি বা SSL কনফিগারেশন সংক্রান্ত সমস্যার কারণে হতে পারে। অনুগ্রহ করে সার্ভার লগ পরীক্ষা করুন।", "connectionLogTitle": "সংযোগ লগ", "connectionLogCopy": "লগগুলি ক্লিপবোর্ডে কপি করুন", "connectionLogEmpty": "এখনো কোনো সংযোগ লগ নেই", "connectionLogWaiting": "সংযোগ লগগুলির জন্য অপেক্ষা করা হচ্ছে...", "connectionLogCopied": "সংযোগ লগগুলি ক্লিপবোর্ডে কপি করা হয়েছে", "connectionLogCopyFailed": "লগগুলি ক্লিপবোর্ডে কপি করতে ব্যর্থ হয়েছে", - "connectionRejected": "সার্ভার কর্তৃক সংযোগ প্রত্যাখ্যাত হয়েছে। অনুগ্রহ করে আপনার প্রমাণীকরণ এবং নেটওয়ার্ক কনফিগারেশন যাচাই করুন।", - "hostKeyRejected": "SSH হোস্ট কী যাচাইকরণ প্রত্যাখ্যাত হয়েছে। সংযোগ বাতিল করা হয়েছে।", - "sessionTakenOver": "সেশনটি অন্য একটি ট্যাবে খোলা হয়েছিল। পুনরায় সংযোগ করা হচ্ছে...", "split": { "splitTab": "স্প্লিট ট্যাব", "addToSplit": "স্প্লিটে যোগ করুন", @@ -2225,55 +2161,6 @@ "sectionApiKeys": "এপিআই কী", "sectionAuditLog": "অডিট লগ", "sectionSsl": "SSL / লেটস এনক্রিপ্ট", - "sectionTouchInput": "টাচ ইনপুট", - "touchEnabled": "টাচ ইনপুট সক্রিয় করুন", - "touchEnabledDesc": "টার্মিনালে এক আঙুলের স্পর্শে স্ক্রোলিং করার অনুমতি দিন", - "touchMomentumEnabled": "গতি সঞ্চার করুন", - "touchMomentumEnabledDesc": "সোয়াইপ করার পর কিছুক্ষণ স্ক্রোল করতে থাকুন।", - "touchAdvanced": "উন্নত", - "touchDragThreshold": "ড্র্যাগ থ্রেশহোল্ড", - "touchMaxWheelDelta": "সর্বোচ্চ চাকার ডেল্টা", - "touchMomentumSampleWindow": "ভরবেগ নমুনা উইন্ডো", - "touchReleaseGracePeriod": "মুক্তির জন্য অতিরিক্ত সময়সীমা", - "touchMinimumVelocity": "সর্বনিম্ন বেগ", - "touchMaximumVelocity": "সর্বোচ্চ বেগ", - "touchMaximumDuration": "সর্বোচ্চ সময়কাল", - "touchMaximumTravel": "সর্বোচ্চ ভ্রমণ", - "touchDecayTime": "ক্ষয়ের সময়", - "touchPixelsPerTick": "প্রতি টিক পিক্সেল", - "touchMaximumFrameInterval": "সর্বোচ্চ ফ্রেম ব্যবধান", - "touchMaximumTicksPerFrame": "প্রতি ফ্রেমে সর্বোচ্চ টিক", - "touchResetDefaults": "ডিফল্টে রিসেট করুন", - "touchSaved": "টাচ ইনপুট সেটিংস সংরক্ষিত হয়েছে", - "touchSaveFailed": "টাচ ইনপুট সেটিংস সংরক্ষণ করতে ব্যর্থ হয়েছে", - "sectionImageStorage": "ইমেজ স্টোরেজ", - "imageStorageMode": "স্টোরেজ মোড", - "imageStorageModeDesc": "যেখানে পেস্ট করা এবং আপলোড করা টার্মিনাল ইমেজগুলো লেখা হয়। কানেকশন অনুযায়ী অটো পিক, লোকাল এবং রিমোট (SFTP) নির্দিষ্ট করা আছে।", - "imageStorageModeAuto": "অটো", - "imageStorageModeLocal": "স্থানীয়", - "imageStorageModeRemoteSftp": "রিমোট (এসএফটিপি)", - "imageStorageLocalDir": "স্থানীয় ডিরেক্টরি", - "imageStorageLocalDirDesc": "টারমিক্স ব্যাকএন্ডে পরম পাথ। নিরাপত্তার কারণে সংরক্ষণের পর দেখানো হয় না।", - "imageStorageLocalDirPlaceholder": "হালনাগাদ রাখতে খালি রাখুন", - "imageStorageHostPath": "হোস্ট পাথ", - "imageStorageHostPathDesc": "টার্মিনাল এজেন্টরা স্থানীয় ডিরেক্টরির যে হোস্ট-সাইড ভিউটি পায়।", - "imageStorageTtl": "টিটিএল (মিলিসেকেন্ড)", - "imageStorageMaxCount": "সর্বোচ্চ ছবির সংখ্যা", - "imageStorageMaxBytes": "সর্বোচ্চ স্টোরেজ বাইট", - "imageStorageSaved": "ছবি সংরক্ষণের সেটিংস সংরক্ষিত হয়েছে", - "imageStorageSaveFailed": "ইমেজ স্টোরেজ সেটিংস সংরক্ষণ করতে ব্যর্থ হয়েছে", - "imageStorageInstanceId": "টার্মিনাল সেশন ইনস্ট্যান্স আইডি", - "imageStorageInstanceIdDesc": "আপনার সংযুক্ত টার্মিনাল ট্যাবগুলোর একটির ইনস্ট্যান্স আইডি, যা দৃশ্যমানতা পরীক্ষা করতে ব্যবহৃত হয়।", - "imageStorageInstanceIdRequired": "প্রথমে একটি টার্মিনাল সেশন ইনস্ট্যান্স আইডি প্রবেশ করান।", - "imageStorageTest": "পরীক্ষা", - "imageStorageTesting": "পরীক্ষা চলছে...", - "imageStorageTestFailed": "ইমেজ স্টোরেজ পরীক্ষা করতে ব্যর্থ হয়েছে", - "imageStorageTestConnected": "টার্মিনাল সেশন সংযুক্ত আছে।", - "imageStorageTestNotConnected": "এই ইনস্ট্যান্স আইডির সাথে কোনো সংযুক্ত টার্মিনাল সেশন নেই।", - "imageStorageTestSelectedMode": "আপলোড করতে ব্যবহৃত হবে: {{mode}}", - "imageStorageSelectedModeUnavailable": "অনুপলব্ধ", - "imageStorageTestLocalVisible": "সংযুক্ত হোস্ট থেকে স্থানীয় ডিরেক্টরিটি দেখা যায়।", - "imageStorageTestLocalNotVisible": "সংযুক্ত হোস্ট থেকে স্থানীয় ডিরেক্টরিটি দেখা যাচ্ছে না।", "sectionBranding": "হোয়াইট লেবেল", "brandingAppName": "আবেদনের নাম", "brandingAppNameDesc": "লগইন স্ক্রিন এবং ব্রাউজার ট্যাবে দেখানো হয়। \"টারমিক্স\" ব্যবহার করতে এটি খালি রাখুন।", @@ -2354,9 +2241,6 @@ "oidcSilentLoginDefaultLockedDesc": "এই সেটিংটি OIDC_SILENT_LOGIN_DEFAULT এনভায়রনমেন্ট ভেরিয়েবল দ্বারা লক করা আছে এবং এখান থেকে এটি পরিবর্তন করা যাবে না।", "allowPasswordReset": "পাসওয়ার্ড রিসেট করার অনুমতি দিন", "allowPasswordResetDesc": "ডকার লগের মাধ্যমে কোড রিসেট করুন", - "commandHistoryEnabled": "কমান্ডের ইতিহাস", - "commandHistoryEnabledDesc": "কমান্ড হিস্ট্রি রেকর্ডিংয়ের অনুমতি দিন। এটি নিষ্ক্রিয় করা থাকলে, হোস্ট-ভিত্তিক সেটিংস নির্বিশেষে হিস্ট্রি সংরক্ষিত হয় না।", - "updateCommandHistoryFailed": "কমান্ড হিস্ট্রি সেটিং আপডেট করতে ব্যর্থ হয়েছে", "analyticsEnabled": "বেনামী ব্যবহারের পরিসংখ্যান শেয়ার করুন", "analyticsEnabledDesc": "টারমিক্সকে উন্নত করতে সাহায্য করার জন্য ব্যবহারকারী, হোস্ট এবং ফিচার ব্যবহারের একটি বেনামী দৈনিক গণনা পাঠায়। এতে কোনো ব্যক্তিগত তথ্য বা সংযোগের বিবরণ অন্তর্ভুক্ত করা হয় না।", "analyticsEnabledLockedDesc": "এই সেটিংটি ENABLE_TELEMETRY এনভায়রনমেন্ট ভেরিয়েবল দ্বারা লক করা আছে এবং এখান থেকে এটি পরিবর্তন করা যাবে না।", @@ -2647,11 +2531,6 @@ "hostDefaultsTerminal": "টার্মিনাল", "hostDefaultsAutoTmux": "অটো-টিমাক্স", "hostDefaultsAutoTmuxDesc": "নতুন হোস্টের টার্মিনালগুলো tmux-এর ভিতরে চালান, যাতে সংযোগ বিচ্ছিন্ন হলেও সেশনগুলো টিকে থাকে (এর জন্য হোস্টে tmux থাকা প্রয়োজন)।", - "terminalSessionTimeout": "টার্মিনাল সেশন স্থায়িত্ব", - "terminalSessionTimeoutDesc": "একটি সংযোগ বিচ্ছিন্ন টার্মিনাল বন্ধ হওয়ার আগে সার্ভারে কতক্ষণ চালু থাকে। সর্বনিম্ন ১ · সর্বোচ্চ ১৪৪০ মিনিট। যে সেশনগুলোকে এই সময়ের চেয়ে বেশি সময় ধরে চালু রাখতে হবে, সেগুলোর জন্য হোস্টে অটো-টিমাক্স (Auto-Tmux) সক্রিয় করুন।", - "terminalSessionTimeoutRange": "১ থেকে ১৪৪০ মিনিটের মধ্যে একটি মান লিখুন।", - "terminalSessionTimeoutSaved": "টার্মিনাল সেশন স্থায়িত্ব সংরক্ষিত হয়েছে", - "terminalSessionTimeoutSaveFailed": "টার্মিনাল সেশন স্থায়িত্ব সংরক্ষণ করতে ব্যর্থ হয়েছে", "minutes": "মিনিট", "hostDefaultsSessionLogging": "সেশন লগিং", "hostDefaultsSessionLoggingDesc": "নতুন হোস্টে ডিফল্টরূপে টার্মিনাল সেশন রেকর্ড করুন।", @@ -2708,15 +2587,6 @@ "connectToTerminal": "টার্মিনালের সাথে সংযোগ করুন", "connectToFiles": "ফাইলগুলির সাথে সংযোগ করুন" }, - "history": { - "noTerminalSelected": "কোন টার্মিনাল নির্বাচন করা হয়নি", - "noTerminalSelectedHint": "এর কমান্ড হিস্ট্রি দেখতে একটি SSH টার্মিনাল ট্যাব খুলুন।", - "searchPlaceholder": "অনুসন্ধানের ইতিহাস...", - "clearAll": "সব পরিষ্কার করুন", - "noHistoryEntries": "ইতিহাসের কোন এন্ট্রি নেই", - "trackingDisabled": "ইতিহাস ট্র্যাকিং নিষ্ক্রিয় করা হয়েছে", - "trackingDisabledHint": "হোস্টের টার্মিনাল সেটিংসে এটি সক্রিয় করুন।" - }, "sshTools": { "keyRecordingTitle": "মূল রেকর্ডিং", "recordToTerminals": "টার্মিনালে রেকর্ড", @@ -3178,30 +3048,11 @@ "terminalToolbar": { "title": "টার্মিনাল টুলবার", "showActions": "টুলবার দেখান", - "hideToolbar": "টুলবার লুকান", - "showToolbar": "টুলবার প্রসারিত করুন", - "moveToolbar": "টুলবার সরান", - "moveToolbarHint": "টুলবারের অবস্থান পরিবর্তন করতে টেনে আনুন", - "layout": "টুলবার প্রদর্শন মোড", - "layoutIcon": "শুধুমাত্র আইকন", - "layoutLabeled": "আইকন এবং লেবেল", - "layoutExpanded": "মেট্রিক্স দিয়ে প্রসারিত", - "image": "ছবির কার্যকলাপ (শুধুমাত্র ছবি)", - "upload": "ছবি আপলোড করুন", - "paste": "ছবি পেস্ট করুন", - "detachTmux": "tmux বিচ্ছিন্ন করুন", - "detachTmuxDescription": "tmux সেশন বিচ্ছিন্ন করুন", - "imageActionFailed": "ছবির অ্যাকশন ব্যর্থ হয়েছে", - "retry": "পুনরায় চেষ্টা করুন", - "dismissError": "ছবির ত্রুটি খারিজ করুন", "cpu": "সিপিইউ", "memory": "স্মৃতি", "disk": "ডিস্ক", "statsUnavailable": "সরাসরি পরিসংখ্যান অনুপলব্ধ", "tmuxDetach": "tmux সেশন বিচ্ছিন্ন করুন", - "uploadImage": "হোস্টে একটি ছবি আপলোড করুন", - "uploadingImage": "আপলোড হচ্ছে...", - "pasteImage": "ক্লিপবোর্ড থেকে ছবি পেস্ট করুন (শুধুমাত্র ছবি)", "openFiles": "খোলা ফাইল", "openDocker": "ওপেন ডকার", "openTunnel": "খোলা টানেল", @@ -3285,31 +3136,6 @@ "timeHours": "{{count}}ঘণ্টা আগে", "timeDays": "{{count}}দিন আগে" }, - "mobileKeyboard": { - "shift": "শিফট", - "ctrl": "Ctrl", - "esc": "এস্ক", - "tab": "ট্যাব", - "backTab": "⇥", - "arrowUp": "তীর ঊর্ধ্বমুখী", - "arrowDown": "অ্যারো ডাউন", - "arrowLeft": "বাম তীর", - "arrowRight": "ডান তীরচিহ্ন", - "home": "হোম", - "end": "শেষ", - "pageUp": "পিজিআপ", - "pageDown": "পিজিডিএন", - "delete": "ডেল", - "paste": "পেস্ট", - "editQuickKeys": "দ্রুত কী সম্পাদনা করুন", - "quickKeysTitle": "দ্রুত চাবি", - "quickKeysDesc": "মুছে ফেলতে × চাপুন। সর্বোচ্চ ৮টি অক্ষর সমর্থন করে।", - "quickKeyPlaceholder": "যেমন sudo ", - "addQuickKey": "যোগ করুন", - "removeQuickKey": "অপসারণ করুন", - "resetDefaults": "ডিফল্টে রিসেট করুন", - "done": "সম্পন্ন হয়েছে" - }, "serial": { "title": "সিরিয়াল", "portLabel": "বন্দর", diff --git a/src/ui/locales/translated/ca_ES.json b/src/ui/locales/translated/ca_ES.json index 809e5376b..1184090e5 100644 --- a/src/ui/locales/translated/ca_ES.json +++ b/src/ui/locales/translated/ca_ES.json @@ -742,7 +742,6 @@ "addHost": "Afegeix amfitrió", "editHost": "Edita l'amfitrió", "cloneHost": "Host clonat", - "enableTerminal": "Habilita el terminal", "enableTunnel": "Habilita el túnel", "enableFileManager": "Habilita el gestor de fitxers", "enableDocker": "Activa Docker", @@ -1041,8 +1040,6 @@ "enableSessionLoggingDesc": "Enregistra la sortida de la sessió de terminal per a una revisió posterior", "allowSessionSharing": "Permetre la compartició de sessions", "allowSessionSharingDesc": "Permet que les sessions en directe d'aquest amfitrió es comparteixin mitjançant un enllaç o amb altres usuaris", - "enableCommandHistory": "Historial de comandaments", - "enableCommandHistoryDesc": "Enregistra les ordres executades en aquest terminal per a l'historial i l'autocompleció", "localEchoLabel": "Eco local", "localEchoDesc": "Prediu localment les pulsacions de tecles imprimibles de manera segura en connexions lentes. Les sol·licituds de contrasenya i l'entrada de control no es reprodueixen mai.", "localEchoDefault": "Utilitza la configuració global", @@ -1072,8 +1069,6 @@ "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", - "enableTerminalToolbar": "Habilita la barra d'eines del terminal", - "enableTerminalToolbarDesc": "Mostra una barra d'eines d'accés ràpid acoblada al terminal actiu per a les eines i les estadístiques en directe d'aquest amfitrió", "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.", @@ -1550,9 +1545,7 @@ "links": "Enllaços", "navigate": "Navega", "select": "Selecciona", - "toggleWith": "Commuta amb", - "localTerminal": "Terminal local", - "localTerminalDesc": "Obre una shell en aquest ordinador" + "toggleWith": "Commuta amb" }, "splitScreen": { "paneEmpty": "Panell {{index}} - buit", @@ -1717,57 +1710,13 @@ "loadSharesFailed": "No s'han pogut carregar les comparticions actives" }, "terminal": { - "sessionExpiredNotice": "La sessió anterior a {{host}} ha caducat després de {{minutes}} minuts de desconnexió; l'intèrpret d'ordres i tot el que s'hi executa ha desaparegut. Activeu Auto-Tmux en aquest amfitrió per mantenir les sessions actives.", - "sessionExpiredNoticeNoMinutes": "La sessió anterior a {{host}} ha caducat mentre estava desconnectada; l'intèrpret d'ordres i tot el que s'hi executa ha desaparegut. Activeu Auto-Tmux en aquest host per mantenir les sessions actives.", - "enableAutoTmuxAction": "Activa la transmissió automàtica", - "autoTmuxEnabled": "Auto-Tmux habilitat per a {{host}}. S'aplica a partir de la següent connexió.", - "autoTmuxEnableFailed": "No s'ha pogut habilitar Auto-Tmux", "connect": "Connecta't a l'amfitrió", "clear": "Clar", "paste": "Enganxa", - "reconnect": "Reconnecta", - "connectionLost": "Connexió perduda", "connected": "Connectat", "clipboardWriteFailed": "No s'ha pogut copiar al porta-retalls. Assegureu-vos que la pàgina es serveix per HTTPS o localhost.", "clipboardReadFailed": "No s'ha pogut llegir del porta-retalls. Assegureu-vos que els permisos del porta-retalls estiguin concedits.", - "clipboardHttpWarning": "No s'ha pogut llegir el porta-retalls. Alguns navegadors restringeixen l'accés al porta-retalls fora d'HTTPS; proveu Ctrl+V o serviu Termix a través d'HTTPS.", - "passwordPromptFillTitle": "Voleu introduir la contrasenya desada en aquesta sol·licitud?", - "unknownError": "S'ha produït un error desconegut", - "websocketError": "Error de connexió de WebSocket", - "connecting": "Connectant...", - "noHostSelected": "No s'ha seleccionat cap amfitrió", - "reconnecting": "Reconnectant... ({{attempt}}/{{max}})", - "reconnected": "S'ha reconnectat correctament", - "tmuxSessionCreated": "Sessió tmux creada: {{name}}", - "tmuxSessionAttached": "sessió tmux adjunta: {{name}}", - "tmuxUnavailable": "tmux no està instal·lat a l'amfitrió remot, tornant a l'intèrpret d'ordres estàndard", - "tmuxSessionPickerTitle": "Sessions de tmux", - "tmuxSessionPickerDesc": "S'han trobat sessions tmux existents en aquest amfitrió. Seleccioneu-ne una per tornar-la a adjuntar o crear una sessió nova.", - "tmuxWindows": "Finestres", - "tmuxWindowCount": "finestra {{count}}", - "tmuxAttached": "Clients adjunts", - "tmuxAttachedCount": "{{count}} adjunt", - "tmuxLastActivity": "Darrera activitat", - "tmuxTimeJustNow": "ara mateix", - "tmuxTimeMinutes": "fa {{count}}minuts", - "tmuxTimeHours": "fa {{count}}h", - "tmuxTimeDays": "fa {{count}}dies", - "tmuxCreateNew": "Inicia una nova sessió", - "tmuxCopyHint": "Ajusta la selecció i prem Intro per copiar al porta-retalls", "tmuxDetach": "Desconnecta de la sessió de tmux", - "tmuxDetached": "Desconnectat de la sessió tmux", - "searchPlaceholder": "Troba", - "searchCaseSensitive": "Coincideix amb el cas", - "searchWholeWord": "Coincideix amb la paraula sencera", - "searchRegex": "Utilitza l'expressió regular", - "searchNoResults": "Sense resultats", - "searchResultCount": "{{index}} de {{count}}", - "searchNext": "Pròxima coincidència (Intro)", - "searchPrevious": "Coincidència anterior (Maj+Intro)", - "searchClose": "Tanca (Escapada)", - "maxReconnectAttemptsReached": "S'ha arribat al màxim d'intents de reconnexió", - "closeTab": "Tanca", - "connectionTimeout": "Temps d'espera de connexió", "terminalTitle": "Terminal - {{host}}", "terminalWithPath": "Terminal - {{host}}:{{path}}", "runTitle": "Executant {{command}} - {{host}}", @@ -1790,27 +1739,14 @@ "opksshOpenBrowser": "Obre el navegador per autenticar-te", "opksshWaitingForAuth": "Esperant l'autenticació al navegador...", "opksshAuthenticating": "Processant l'autenticació...", - "opksshTimeout": "S'ha esgotat el temps d'autenticació. Torna-ho a provar.", - "opksshAuthFailed": "L'autenticació ha fallat. Si us plau, comproveu les vostres credencials i torneu-ho a intentar.", "opksshSignInWith": "Inicia la sessió amb {{provider}}", - "vaultAuthTitle": "Cal iniciar sessió a la caixa forta", - "vaultAuthDescription": "S'ha obert una finestra per iniciar la sessió a HashiCorp Vault. Completeu l'inici de sessió allà; aquesta connexió continuarà automàticament.", - "vaultAuthFailed": "L'autenticació de la caixa forta ha fallat. Torna-ho a provar.", - "vaultReopen": "Torna a obrir la finestra d'inici de sessió", "sudoPasswordPopupTitle": "Voleu inserir la contrasenya?", - "linkDialogTitle": "Obre l'enllaç", - "linkDialogOpen": "Obre", - "linkDialogCopy": "Còpia", - "websocketAbnormalClose": "La connexió s'ha tancat inesperadament. Això pot ser degut a un problema de configuració del proxy invers o de l'SSL. Si us plau, reviseu els registres del servidor.", "connectionLogTitle": "Registre de connexió", "connectionLogCopy": "Copia els registres al porta-retalls", "connectionLogEmpty": "Encara no hi ha registres de connexió", "connectionLogWaiting": "Esperant els registres de connexió...", "connectionLogCopied": "Registres de connexió copiats al porta-retalls", "connectionLogCopyFailed": "No s'han pogut copiar els registres al porta-retalls", - "connectionRejected": "Connexió rebutjada pel servidor. Si us plau, comproveu l'autenticació i la configuració de xarxa.", - "hostKeyRejected": "Verificació de la clau de l'amfitrió SSH rebutjada. Connexió cancel·lada.", - "sessionTakenOver": "La sessió s'ha obert en una altra pestanya. S'està tornant a connectar...", "split": { "splitTab": "Pestanya dividida", "addToSplit": "Afegeix a Dividir", @@ -2225,55 +2161,6 @@ "sectionApiKeys": "Claus API", "sectionAuditLog": "Registre d'auditoria", "sectionSsl": "SSL / Xifrem", - "sectionTouchInput": "Entrada tàctil", - "touchEnabled": "Activa l'entrada tàctil", - "touchEnabledDesc": "Permetre el desplaçament tàctil amb un dit als terminals", - "touchMomentumEnabled": "Activa l'impuls", - "touchMomentumEnabledDesc": "Continua desplaçant-te breument després de lliscar el dit", - "touchAdvanced": "Avançat", - "touchDragThreshold": "Llindar d'arrossegament", - "touchMaxWheelDelta": "Delta màxim de roda", - "touchMomentumSampleWindow": "Finestra de mostra de momentum", - "touchReleaseGracePeriod": "Període de gràcia de llançament", - "touchMinimumVelocity": "Velocitat mínima", - "touchMaximumVelocity": "Velocitat màxima", - "touchMaximumDuration": "Durada màxima", - "touchMaximumTravel": "Recorregut màxim", - "touchDecayTime": "Temps de decadència", - "touchPixelsPerTick": "Píxels per clic", - "touchMaximumFrameInterval": "Interval màxim de fotograma", - "touchMaximumTicksPerFrame": "Màxims de tics per fotograma", - "touchResetDefaults": "Restableix els valors predeterminats", - "touchSaved": "Configuració d'entrada tàctil desada", - "touchSaveFailed": "No s'ha pogut desar la configuració d'entrada tàctil", - "sectionImageStorage": "Emmagatzematge d'imatges", - "imageStorageMode": "Mode d'emmagatzematge", - "imageStorageModeDesc": "On s'escriuen les imatges de terminal enganxades i carregades. Les seleccions automàtiques per connexió, Local i Remota (SFTP) estan corregides.", - "imageStorageModeAuto": "Automàtic", - "imageStorageModeLocal": "Local", - "imageStorageModeRemoteSftp": "Remot (SFTP)", - "imageStorageLocalDir": "Directori local", - "imageStorageLocalDirDesc": "Ruta absoluta al backend de Termix. No es mostra després de desar per seguretat.", - "imageStorageLocalDirPlaceholder": "Deixeu-ho en blanc per mantenir-ho actualitzat", - "imageStorageHostPath": "Camí d'amfitrió", - "imageStorageHostPathDesc": "La vista del costat de l'amfitrió del directori local que reben els agents del terminal.", - "imageStorageTtl": "TTL (ms)", - "imageStorageMaxCount": "Nombre màxim d'imatges", - "imageStorageMaxBytes": "Bytes d'emmagatzematge màxims", - "imageStorageSaved": "S'ha desat la configuració d'emmagatzematge d'imatges", - "imageStorageSaveFailed": "No s'ha pogut desar la configuració d'emmagatzematge d'imatges", - "imageStorageInstanceId": "ID d'instància de sessió de terminal", - "imageStorageInstanceIdDesc": "ID d'instància d'una de les pestanyes del terminal connectat, que s'utilitza per provar la visibilitat.", - "imageStorageInstanceIdRequired": "Introduïu primer un ID d'instància de sessió de terminal", - "imageStorageTest": "Prova", - "imageStorageTesting": "Proves...", - "imageStorageTestFailed": "No s'ha pogut provar l'emmagatzematge d'imatges", - "imageStorageTestConnected": "La sessió de terminal està connectada.", - "imageStorageTestNotConnected": "No hi ha cap sessió de terminal connectada amb aquest ID d'instància.", - "imageStorageTestSelectedMode": "Una càrrega utilitzaria: {{mode}}", - "imageStorageSelectedModeUnavailable": "No disponible", - "imageStorageTestLocalVisible": "El directori local és visible des de l'amfitrió connectat.", - "imageStorageTestLocalNotVisible": "El directori local NO és visible des de l'amfitrió connectat.", "sectionBranding": "Marca blanca", "brandingAppName": "Nom de l'aplicació", "brandingAppNameDesc": "Es mostra a la pantalla d'inici de sessió i a la pestanya del navegador. Deixeu-ho en blanc per utilitzar \"Termix\".", @@ -2354,9 +2241,6 @@ "oidcSilentLoginDefaultLockedDesc": "Aquesta configuració està bloquejada per la variable d'entorn OIDC_SILENT_LOGIN_DEFAULT i no es pot canviar aquí.", "allowPasswordReset": "Permet el restabliment de la contrasenya", "allowPasswordResetDesc": "Restableix el codi a través dels registres de Docker", - "commandHistoryEnabled": "Historial de comandaments", - "commandHistoryEnabledDesc": "Permet l'enregistrament de l'historial d'ordres. Quan està desactivat, l'historial no es desa independentment de la configuració de cada host.", - "updateCommandHistoryFailed": "No s'ha pogut actualitzar la configuració de l'historial d'ordres", "analyticsEnabled": "Comparteix estadístiques d'ús anònimes", "analyticsEnabledDesc": "Envia un recompte diari anònim d'usuaris, amfitrions i ús de funcions per ajudar a millorar Termix. No s'hi inclouen mai dades personals ni detalls de connexió.", "analyticsEnabledLockedDesc": "Aquesta configuració està bloquejada per la variable d'entorn ENABLE_TELEMETRY i no es pot canviar aquí.", @@ -2647,11 +2531,6 @@ "hostDefaultsTerminal": "Terminal", "hostDefaultsAutoTmux": "Auto-Tmux", "hostDefaultsAutoTmuxDesc": "Executa els terminals dels nous hosts dins de tmux perquè les sessions sobrevisquin a les desconnexions (necessita tmux al host)", - "terminalSessionTimeout": "Persistència de la sessió terminal", - "terminalSessionTimeoutDesc": "Quant de temps es manté actiu un terminal desconnectat al servidor abans de tancar-lo. Mín 1 · Màx 1440 minuts. Per a sessions que han de durar més temps, activeu Auto-Tmux a l'amfitrió.", - "terminalSessionTimeoutRange": "Introduïu un valor entre 1 i 1440 minuts", - "terminalSessionTimeoutSaved": "Persistència de la sessió de terminal desada", - "terminalSessionTimeoutSaveFailed": "No s'ha pogut desar la persistència de la sessió del terminal", "minutes": "minuts", "hostDefaultsSessionLogging": "Registre de sessions", "hostDefaultsSessionLoggingDesc": "Enregistra les sessions de terminal en els nous hosts per defecte", @@ -2708,15 +2587,6 @@ "connectToTerminal": "Connecta't al terminal", "connectToFiles": "Connecta't a fitxers" }, - "history": { - "noTerminalSelected": "No s'ha seleccionat cap terminal", - "noTerminalSelectedHint": "Obriu una pestanya de terminal SSH per veure el seu historial d'ordres", - "searchPlaceholder": "Historial de cerca...", - "clearAll": "Esborra-ho tot", - "noHistoryEntries": "Sense entrades a l'historial", - "trackingDisabled": "El seguiment de l'historial està desactivat", - "trackingDisabledHint": "Activeu-ho a la configuració del terminal de l'amfitrió." - }, "sshTools": { "keyRecordingTitle": "Enregistrament de claus", "recordToTerminals": "Registre als terminals", @@ -3178,30 +3048,11 @@ "terminalToolbar": { "title": "Barra d'eines del terminal", "showActions": "Mostra la barra d'eines", - "hideToolbar": "Amaga la barra d'eines", - "showToolbar": "Expandeix la barra d'eines", - "moveToolbar": "Mou la barra d'eines", - "moveToolbarHint": "Arrossega per reposicionar la barra d'eines", - "layout": "Mode de visualització de la barra d'eines", - "layoutIcon": "Només icona", - "layoutLabeled": "Icones i etiquetes", - "layoutExpanded": "Ampliat amb mètriques", - "image": "Accions d'imatge (només imatges)", - "upload": "Puja una imatge", - "paste": "Enganxa la imatge", - "detachTmux": "Desconnecta tmux", - "detachTmuxDescription": "Desconnecta la sessió tmux", - "imageActionFailed": "L'acció de la imatge ha fallat", - "retry": "Torna-ho a intentar", - "dismissError": "Error d'ignorar la imatge", "cpu": "CPU", "memory": "Memòria", "disk": "Disc", "statsUnavailable": "Estadístiques en directe no disponibles", "tmuxDetach": "Desconnecta la sessió tmux", - "uploadImage": "Puja una imatge a l'amfitrió", - "uploadingImage": "S'està carregant...", - "pasteImage": "Enganxa la imatge del porta-retalls (només imatges)", "openFiles": "Obre els fitxers", "openDocker": "Obre Docker", "openTunnel": "Túnel obert", @@ -3285,31 +3136,6 @@ "timeHours": "fa {{count}}h", "timeDays": "fa {{count}}dies" }, - "mobileKeyboard": { - "shift": "Majúscules", - "ctrl": "Ctrl", - "esc": "Esc", - "tab": "Pestanya", - "backTab": "⇥", - "arrowUp": "Fletxa amunt", - "arrowDown": "Fletxa avall", - "arrowLeft": "Fletxa esquerra", - "arrowRight": "Fletxa dreta", - "home": "Inici", - "end": "Final", - "pageUp": "Re Pàg", - "pageDown": "Av Pàg", - "delete": "Eliminar", - "paste": "Enganxa", - "editQuickKeys": "Edita les tecles ràpides", - "quickKeysTitle": "Tecles ràpides", - "quickKeysDesc": "Toca × per eliminar. Admet fins a 8 caràcters.", - "quickKeyPlaceholder": "p. ex. sudo ", - "addQuickKey": "Afegeix", - "removeQuickKey": "Elimina", - "resetDefaults": "Restableix els valors predeterminats", - "done": "Fet" - }, "serial": { "title": "Sèrie", "portLabel": "Port", diff --git a/src/ui/locales/translated/cs_CZ.json b/src/ui/locales/translated/cs_CZ.json index 9953518c2..ceeebd90f 100644 --- a/src/ui/locales/translated/cs_CZ.json +++ b/src/ui/locales/translated/cs_CZ.json @@ -742,7 +742,6 @@ "addHost": "Přidat hostitele", "editHost": "Upravit hostitele", "cloneHost": "Klonovat hostitele", - "enableTerminal": "Povolit terminál", "enableTunnel": "Povolit tunel", "enableFileManager": "Povolit Správce souborů", "enableDocker": "Povolit Docker", @@ -1041,8 +1040,6 @@ "enableSessionLoggingDesc": "Zaznamenejte výstup terminálové relace pro pozdější kontrolu", "allowSessionSharing": "Povolit sdílení relace", "allowSessionSharingDesc": "Umožnit sdílení živých relací na tomto hostiteli prostřednictvím odkazu nebo s ostatními uživateli", - "enableCommandHistory": "Historie příkazů", - "enableCommandHistoryDesc": "Zaznamenávejte příkazy spouštěné v tomto terminálu pro historii a automatické dokončování", "localEchoLabel": "Místní ozvěna", "localEchoDesc": "Předvídat bezpečně tisknutelné stisky kláves lokálně na pomalém připojení. Výzvy k zadání hesla a ovládací prvky se nikdy nezobrazují.", "localEchoDefault": "Použít globální nastavení", @@ -1072,8 +1069,6 @@ "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", - "enableTerminalToolbar": "Povolit panel nástrojů terminálu", - "enableTerminalToolbarDesc": "Zobrazit panel nástrojů pro rychlý přístup ukotvený v aktivním terminálu pro nástroje a aktuální statistiky tohoto hostitele", "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ů.", @@ -1550,9 +1545,7 @@ "links": "Odkazy", "navigate": "Navigovat", "select": "Vybrat", - "toggleWith": "Přepnout pomocí", - "localTerminal": "Místní terminál", - "localTerminalDesc": "Otevřít shell na tomto počítači" + "toggleWith": "Přepnout pomocí" }, "splitScreen": { "paneEmpty": "Podokno {{index}} – prázdné", @@ -1717,57 +1710,13 @@ "loadSharesFailed": "Nepodařilo se načíst aktivní sdílení" }, "terminal": { - "sessionExpiredNotice": "Vaše předchozí relace na {{host}} vypršela po {{minutes}} minutách odpojení; shell a vše, co v něm běží, je pryč. Povolte na tomto hostiteli Auto-Tmux, aby relace zůstaly aktivní.", - "sessionExpiredNoticeNoMinutes": "Vaše předchozí relace na {{host}} vypršela během odpojení; shell a vše, co v něm běží, je pryč. Povolte na tomto hostiteli Auto-Tmux, aby relace zůstaly aktivní.", - "enableAutoTmuxAction": "Povolit Auto-Tmux", - "autoTmuxEnabled": "Auto-Tmux povolen pro {{host}}. Platí od dalšího připojení.", - "autoTmuxEnableFailed": "Nepodařilo se povolit Auto-Tmux", "connect": "Připojení k hostiteli", "clear": "Jasný", "paste": "Pasta", - "reconnect": "Znovu se připojte", - "connectionLost": "Spojení ztraceno", "connected": "Připojeno", "clipboardWriteFailed": "Kopírování do schránky se nezdařilo. Ujistěte se, že stránka je zobrazována přes HTTPS nebo localhost.", "clipboardReadFailed": "Čtení ze schránky se nezdařilo. Ujistěte se, že máte povolena přístup ke schránce.", - "clipboardHttpWarning": "Nelze přečíst schránku. Některé prohlížeče omezují přístup ke schránce mimo HTTPS – zkuste Ctrl+V nebo přes HTTPS poskytněte Termix.", - "passwordPromptFillTitle": "Vyplňte uložené heslo do tohoto řádku?", - "unknownError": "Došlo k neznámé chybě", - "websocketError": "Chyba připojení k WebSocketu", - "connecting": "Spojovací...", - "noHostSelected": "Nebyl vybrán žádný hostitel", - "reconnecting": "Znovu se připojuji... ({{attempt}}/{{max}})", - "reconnected": "Úspěšné opětovné připojení", - "tmuxSessionCreated": "Vytvořena relace tmux: {{name}}", - "tmuxSessionAttached": "připojena relace tmux: {{name}}", - "tmuxUnavailable": "tmux není nainstalován na vzdáleném hostiteli, vrací se ke standardnímu shellu.", - "tmuxSessionPickerTitle": "Sezení tmux", - "tmuxSessionPickerDesc": "Na tomto hostiteli byly nalezeny existující relace tmux. Vyberte jednu, kterou chcete znovu připojit, nebo vytvořte novou relaci.", - "tmuxWindows": "Okna", - "tmuxWindowCount": "{{count}} okno", - "tmuxAttached": "Připojení klienti", - "tmuxAttachedCount": "{{count}} připojen", - "tmuxLastActivity": "Poslední aktivita", - "tmuxTimeJustNow": "právě teď", - "tmuxTimeMinutes": "{{count}}před m", - "tmuxTimeHours": "{{count}}před hodinami", - "tmuxTimeDays": "Před {{count}}dny", - "tmuxCreateNew": "Zahájit novou relaci", - "tmuxCopyHint": "Upravte výběr a stiskněte Enter pro zkopírování do schránky", "tmuxDetach": "Odpojit se od relace tmux", - "tmuxDetached": "Odpojeno od relace tmux", - "searchPlaceholder": "Nalézt", - "searchCaseSensitive": "Zápasová karta", - "searchWholeWord": "Shoda celého slova", - "searchRegex": "Použít regulární výraz", - "searchNoResults": "Žádné výsledky", - "searchResultCount": "{{index}} z {{count}}", - "searchNext": "Další zápas (Enter)", - "searchPrevious": "Předchozí shoda (Shift+Enter)", - "searchClose": "Zavřít (Escape)", - "maxReconnectAttemptsReached": "Dosažen maximální počet pokusů o opětovné připojení", - "closeTab": "Blízko", - "connectionTimeout": "Časový limit připojení", "terminalTitle": "Terminál - {{host}}", "terminalWithPath": "Terminál - {{host}}:{{path}}", "runTitle": "Spuštěno {{command}} - {{host}}", @@ -1790,27 +1739,14 @@ "opksshOpenBrowser": "Otevřít prohlížeč pro ověření", "opksshWaitingForAuth": "Čekání na ověření v prohlížeči...", "opksshAuthenticating": "Zpracování ověřování...", - "opksshTimeout": "Časový limit ověření vypršel. Zkuste to prosím znovu.", - "opksshAuthFailed": "Ověření se nezdařilo. Zkontrolujte prosím své přihlašovací údaje a zkuste to znovu.", "opksshSignInWith": "Přihlásit se pomocí {{provider}}", - "vaultAuthTitle": "Vyžaduje se přihlášení do trezoru", - "vaultAuthDescription": "Otevřelo se okno pro přihlášení do HashiCorp Vault. Dokončete přihlášení tam; toto připojení bude automaticky pokračovat.", - "vaultAuthFailed": "Ověření v trezoru se nezdařilo. Zkuste to prosím znovu.", - "vaultReopen": "Znovu otevřít přihlašovací okno", "sudoPasswordPopupTitle": "Vložit heslo?", - "linkDialogTitle": "Otevřít odkaz", - "linkDialogOpen": "OTEVŘENO", - "linkDialogCopy": "Kopie", - "websocketAbnormalClose": "Spojení se neočekávaně ukončilo. Může to být způsobeno problémem s reverzní proxy nebo konfigurací SSL. Zkontrolujte prosím protokoly serveru.", "connectionLogTitle": "Protokol připojení", "connectionLogCopy": "Kopírování protokolů do schránky", "connectionLogEmpty": "Zatím žádné protokoly připojení", "connectionLogWaiting": "Čekání na protokoly připojení...", "connectionLogCopied": "Protokoly připojení zkopírovány do schránky", "connectionLogCopyFailed": "Nepodařilo se zkopírovat protokoly do schránky", - "connectionRejected": "Server odmítl připojení. Zkontrolujte prosím ověření a konfiguraci sítě.", - "hostKeyRejected": "Ověření klíče hostitele SSH odmítnuto. Připojení zrušeno.", - "sessionTakenOver": "Relace byla otevřena na jiné kartě. Znovu se připojuji...", "split": { "splitTab": "Rozdělit kartu", "addToSplit": "Přidat do Splitu", @@ -2225,55 +2161,6 @@ "sectionApiKeys": "Klíče API", "sectionAuditLog": "Protokol auditu", "sectionSsl": "SSL / Pojďme šifrovat", - "sectionTouchInput": "Dotykový vstup", - "touchEnabled": "Povolit dotykový vstup", - "touchEnabledDesc": "Povolit posouvání jedním prstem v terminálech", - "touchMomentumEnabled": "Povolit hybnost", - "touchMomentumEnabledDesc": "Po přejetí prstem krátce pokračujte v rolování", - "touchAdvanced": "Moderní", - "touchDragThreshold": "Práh přetažení", - "touchMaxWheelDelta": "Maximální delta kola", - "touchMomentumSampleWindow": "Okno vzorku hybnosti", - "touchReleaseGracePeriod": "Dodatečná lhůta", - "touchMinimumVelocity": "Minimální rychlost", - "touchMaximumVelocity": "Maximální rychlost", - "touchMaximumDuration": "Maximální doba trvání", - "touchMaximumTravel": "Maximální zdvih", - "touchDecayTime": "Doba rozpadu", - "touchPixelsPerTick": "Pixely na tik", - "touchMaximumFrameInterval": "Maximální interval snímků", - "touchMaximumTicksPerFrame": "Maximální počet tiků na snímek", - "touchResetDefaults": "Obnovit na výchozí nastavení", - "touchSaved": "Nastavení dotykového vstupu uloženo", - "touchSaveFailed": "Nepodařilo se uložit nastavení dotykového vstupu", - "sectionImageStorage": "Ukládání obrázků", - "imageStorageMode": "Režim úložiště", - "imageStorageModeDesc": "Kam se zapisují vložené a nahrané obrazy terminálu. Automatické výběry podle připojení, lokální a vzdálené (SFTP) jsou opraveny.", - "imageStorageModeAuto": "Auto", - "imageStorageModeLocal": "Místní", - "imageStorageModeRemoteSftp": "Vzdálené (SFTP)", - "imageStorageLocalDir": "Místní adresář", - "imageStorageLocalDirDesc": "Absolutní cesta na serveru Termix. Po uložení se z bezpečnostních důvodů nezobrazuje.", - "imageStorageLocalDirPlaceholder": "Pro zachování aktuálního stavu ponechte prázdné", - "imageStorageHostPath": "Cesta hostitele", - "imageStorageHostPathDesc": "Pohled na lokální adresář, který termináloví agenti přijímají, na straně hostitele.", - "imageStorageTtl": "Doba trvání (ms)", - "imageStorageMaxCount": "Maximální počet obrázků", - "imageStorageMaxBytes": "Maximální počet bajtů úložiště", - "imageStorageSaved": "Nastavení ukládání obrázků uloženo", - "imageStorageSaveFailed": "Nepodařilo se uložit nastavení úložiště obrázků", - "imageStorageInstanceId": "ID instance terminálové relace", - "imageStorageInstanceIdDesc": "ID instance jedné z vašich připojených karet terminálu, používané k testování viditelnosti.", - "imageStorageInstanceIdRequired": "Nejprve zadejte ID instance terminálové relace", - "imageStorageTest": "Test", - "imageStorageTesting": "Testování...", - "imageStorageTestFailed": "Nepodařilo se otestovat úložiště obrázků", - "imageStorageTestConnected": "Terminálová relace je připojena.", - "imageStorageTestNotConnected": "S tímto ID instance není připojena žádná terminálová relace.", - "imageStorageTestSelectedMode": "Nahrávání by použilo: {{mode}}", - "imageStorageSelectedModeUnavailable": "Nedostupné", - "imageStorageTestLocalVisible": "Lokální adresář je viditelný z připojeného hostitele.", - "imageStorageTestLocalNotVisible": "Lokální adresář NENÍ viditelný z připojeného hostitele.", "sectionBranding": "Bílý štítek", "brandingAppName": "Název aplikace", "brandingAppNameDesc": "Zobrazuje se na přihlašovací obrazovce a kartě prohlížeče. Pro použití „Termix“ ponechte pole prázdné.", @@ -2354,9 +2241,6 @@ "oidcSilentLoginDefaultLockedDesc": "Toto nastavení je uzamčeno proměnnou prostředí OIDC_SILENT_LOGIN_DEFAULT a nelze jej zde změnit.", "allowPasswordReset": "Povolit resetování hesla", "allowPasswordResetDesc": "Reset kódu pomocí protokolů Dockeru", - "commandHistoryEnabled": "Historie příkazů", - "commandHistoryEnabledDesc": "Povolit záznam historie příkazů. Pokud je tato možnost zakázána, historie se neukládá bez ohledu na nastavení pro jednotlivé hostitele.", - "updateCommandHistoryFailed": "Nepodařilo se aktualizovat nastavení historie příkazů.", "analyticsEnabled": "Sdílení anonymních statistik používání", "analyticsEnabledDesc": "Odesílá anonymní denní počet uživatelů, hostitelů a využití funkcí, aby pomohl vylepšit Termix. Nikdy neobsahuje žádné osobní údaje ani podrobnosti o připojení.", "analyticsEnabledLockedDesc": "Toto nastavení je uzamčeno proměnnou prostředí ENABLE_TELEMETRY a nelze jej zde změnit.", @@ -2647,11 +2531,6 @@ "hostDefaultsTerminal": "Terminál", "hostDefaultsAutoTmux": "Auto-Tmux", "hostDefaultsAutoTmuxDesc": "Spouštět terminály nových hostitelů uvnitř tmuxu, aby relace přežily odpojení (vyžaduje tmux na hostiteli)", - "terminalSessionTimeout": "Perzistence terminálové relace", - "terminalSessionTimeoutDesc": "Jak dlouho je odpojený terminál udržován aktivní na serveru, než je uzavřen. Min. 1 · Max. 1440 minut. Pro relace, které musí tuto dobu přetrvávat, povolte na hostiteli Auto-Tmux.", - "terminalSessionTimeoutRange": "Zadejte hodnotu mezi 1 a 1440 minutami", - "terminalSessionTimeoutSaved": "Trvalost terminálové relace uložena", - "terminalSessionTimeoutSaveFailed": "Nepodařilo se uložit trvalost terminálové relace", "minutes": "zápis", "hostDefaultsSessionLogging": "Protokolování relací", "hostDefaultsSessionLoggingDesc": "Ve výchozím nastavení zaznamenává terminálové relace na nových hostitelích", @@ -2708,15 +2587,6 @@ "connectToTerminal": "Připojení k terminálu", "connectToFiles": "Připojení k souborům" }, - "history": { - "noTerminalSelected": "Není vybrán žádný terminál", - "noTerminalSelectedHint": "Otevření karty terminálu SSH pro zobrazení historie jejích příkazů", - "searchPlaceholder": "Historie vyhledávání...", - "clearAll": "Vymazat vše", - "noHistoryEntries": "Žádné záznamy v historii", - "trackingDisabled": "Sledování historie je zakázáno", - "trackingDisabledHint": "Povolte to v nastavení terminálu hostitele." - }, "sshTools": { "keyRecordingTitle": "Nahrávání klíčů", "recordToTerminals": "Záznam do terminálů", @@ -3178,30 +3048,11 @@ "terminalToolbar": { "title": "Panel nástrojů terminálu", "showActions": "Zobrazit panel nástrojů", - "hideToolbar": "Skrýt panel nástrojů", - "showToolbar": "Rozbalit panel nástrojů", - "moveToolbar": "Přesunout panel nástrojů", - "moveToolbarHint": "Přetažením změňte umístění panelu nástrojů", - "layout": "Režim zobrazení panelu nástrojů", - "layoutIcon": "Pouze ikona", - "layoutLabeled": "Ikony a popisky", - "layoutExpanded": "Rozšířeno o metriky", - "image": "Akce s obrázky (pouze obrázky)", - "upload": "Nahrát obrázek", - "paste": "Vložit obrázek", - "detachTmux": "Odpojit tmux", - "detachTmuxDescription": "Odpojení relace tmux", - "imageActionFailed": "Akce s obrázkem se nezdařila", - "retry": "Zkusit znovu", - "dismissError": "Chyba při zavírání obrázku", "cpu": "Procesor", "memory": "Paměť", "disk": "Disk", "statsUnavailable": "Živé statistiky nejsou k dispozici", "tmuxDetach": "Odpojit relaci tmux", - "uploadImage": "Nahrajte obrázek na hostitele", - "uploadingImage": "Nahrávání...", - "pasteImage": "Vložit obrázek ze schránky (pouze obrázky)", "openFiles": "Otevřít soubory", "openDocker": "Otevřít Docker", "openTunnel": "Otevřený tunel", @@ -3285,31 +3136,6 @@ "timeHours": "{{count}}před hodinami", "timeDays": "Před {{count}}dny" }, - "mobileKeyboard": { - "shift": "Posun", - "ctrl": "Ctrl", - "esc": "Esc", - "tab": "Záložka", - "backTab": "⇥", - "arrowUp": "Šipka nahoru", - "arrowDown": "Šipka dolů", - "arrowLeft": "Šipka vlevo", - "arrowRight": "Šipka vpravo", - "home": "Domov", - "end": "Konec", - "pageUp": "PgUp", - "pageDown": "PgDn", - "delete": "Del", - "paste": "Pasta", - "editQuickKeys": "Upravit rychlé klávesy", - "quickKeysTitle": "Rychlé klávesy", - "quickKeysDesc": "Klepnutím na × odstraníte. Podporuje až 8 znaků.", - "quickKeyPlaceholder": "např. sudo ", - "addQuickKey": "Přidat", - "removeQuickKey": "Odstranit", - "resetDefaults": "Obnovit na výchozí nastavení", - "done": "Hotovo" - }, "serial": { "title": "Seriál", "portLabel": "Přístav", diff --git a/src/ui/locales/translated/da_DK.json b/src/ui/locales/translated/da_DK.json index dddce36a4..fc23d8904 100644 --- a/src/ui/locales/translated/da_DK.json +++ b/src/ui/locales/translated/da_DK.json @@ -742,7 +742,6 @@ "addHost": "Tilføj vært", "editHost": "Rediger vært", "cloneHost": "Klon vært", - "enableTerminal": "Aktivér terminal", "enableTunnel": "Aktivér tunnel", "enableFileManager": "Aktivér Filhåndtering", "enableDocker": "Aktivér Docker", @@ -1041,8 +1040,6 @@ "enableSessionLoggingDesc": "Optag output fra terminalsessionen til senere gennemgang", "allowSessionSharing": "Tillad deling af sessioner", "allowSessionSharingDesc": "Tillad live-sessioner på denne vært at blive delt via link eller med andre brugere", - "enableCommandHistory": "Kommandohistorik", - "enableCommandHistoryDesc": "Optagelseskommandoer køres i denne terminal for historik og autofuldførelse", "localEchoLabel": "Lokalt ekko", "localEchoDesc": "Forudsig sikre, udskrivbare tastetryk lokalt på langsomme forbindelser. Adgangskodeprompter og kontrolinput gentages aldrig.", "localEchoDefault": "Brug global indstilling", @@ -1072,8 +1069,6 @@ "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", - "enableTerminalToolbar": "Aktivér Terminalværktøjslinjen", - "enableTerminalToolbarDesc": "Vis en værktøjslinje med hurtig adgang, der er docket til den aktive terminal for denne værts værktøjer og live statistikker", "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.", @@ -1550,9 +1545,7 @@ "links": "Links", "navigate": "Navigere", "select": "Vælge", - "toggleWith": "Skift med", - "localTerminal": "Lokal terminal", - "localTerminalDesc": "Åbn en shell på denne computer" + "toggleWith": "Skift med" }, "splitScreen": { "paneEmpty": "Panel {{index}} - tom", @@ -1717,57 +1710,13 @@ "loadSharesFailed": "Kunne ikke indlæse aktive delinger" }, "terminal": { - "sessionExpiredNotice": "Din forrige session på {{host}} udløb efter {{minutes}} minutters afbrydelse; shell'en og alt, der kører i den, er væk. Aktiver Auto-Tmux på denne vært for at holde sessionerne aktive.", - "sessionExpiredNoticeNoMinutes": "Din forrige session på {{host}} udløb, mens forbindelsen var afbrudt; skallen og alt, der kørte i den, er væk. Aktiver Auto-Tmux på denne vært for at holde sessionerne aktive.", - "enableAutoTmuxAction": "Aktivér Auto-Tmux", - "autoTmuxEnabled": "Auto-Tmux aktiveret for {{host}}. Det gælder fra den næste forbindelse.", - "autoTmuxEnableFailed": "Kunne ikke aktivere Auto-Tmux", "connect": "Opret forbindelse til vært", "clear": "Klar", "paste": "Indsæt", - "reconnect": "Genopret forbindelse", - "connectionLost": "Forbindelse tabt", "connected": "Forbundet", "clipboardWriteFailed": "Kunne ikke kopiere til udklipsholder. Sørg for, at siden serveres via HTTPS eller localhost.", "clipboardReadFailed": "Kunne ikke læse fra udklipsholderen. Sørg for, at der er givet tilladelse til udklipsholderen.", - "clipboardHttpWarning": "Kunne ikke læse udklipsholderen. Nogle browsere begrænser adgang til udklipsholderen uden for HTTPS - prøv Ctrl+V i stedet, eller server Termix via HTTPS.", - "passwordPromptFillTitle": "Udfyld den gemte adgangskode i denne prompt?", - "unknownError": "Der opstod en ukendt fejl", - "websocketError": "WebSocket-forbindelsesfejl", - "connecting": "Forbinder...", - "noHostSelected": "Ingen vært valgt", - "reconnecting": "Genopretter forbindelse... ({{attempt}}/{{max}})", - "reconnected": "Genoprettet forbindelse", - "tmuxSessionCreated": "tmux-session oprettet: {{name}}", - "tmuxSessionAttached": "tmux-session vedhæftet: {{name}}", - "tmuxUnavailable": "tmux er ikke installeret på den eksterne vært, og bruger standardshell", - "tmuxSessionPickerTitle": "tmux-sessioner", - "tmuxSessionPickerDesc": "Eksisterende tmux-sessioner fundet på denne vært. Vælg en for at gentilknytte eller opret en ny session.", - "tmuxWindows": "Vinduer", - "tmuxWindowCount": "{{count}} vindue", - "tmuxAttached": "Tilknyttede klienter", - "tmuxAttachedCount": "{{count}} vedhæftet", - "tmuxLastActivity": "Sidste aktivitet", - "tmuxTimeJustNow": "lige nu", - "tmuxTimeMinutes": "For {{count}}minutter siden", - "tmuxTimeHours": "For {{count}}timer siden", - "tmuxTimeDays": "for {{count}}dage siden", - "tmuxCreateNew": "Start en ny session", - "tmuxCopyHint": "Juster valget, og tryk på Enter for at kopiere til udklipsholderen", "tmuxDetach": "Afbryd forbindelsen til tmux-sessionen", - "tmuxDetached": "Afkoblet fra tmux-session", - "searchPlaceholder": "Finde", - "searchCaseSensitive": "Match-sag", - "searchWholeWord": "Match hele ordet", - "searchRegex": "Brug regulært udtryk", - "searchNoResults": "Ingen resultater", - "searchResultCount": "{{index}} af {{count}}", - "searchNext": "Næste kamp (Enter)", - "searchPrevious": "Forrige match (Shift+Enter)", - "searchClose": "Luk (Escape)", - "maxReconnectAttemptsReached": "Maksimalt antal forsøg på genoprettelse nået", - "closeTab": "Tæt", - "connectionTimeout": "Forbindelsestimeout", "terminalTitle": "Terminal - {{host}}", "terminalWithPath": "Terminal - {{host}}:{{path}}", "runTitle": "Kører {{command}} - {{host}}", @@ -1790,27 +1739,14 @@ "opksshOpenBrowser": "Åbn browseren for at godkende", "opksshWaitingForAuth": "Venter på godkendelse i browseren...", "opksshAuthenticating": "Behandler godkendelse...", - "opksshTimeout": "Godkendelsen udløb. Prøv igen.", - "opksshAuthFailed": "Godkendelse mislykkedes. Tjek dine loginoplysninger, og prøv igen.", "opksshSignInWith": "Log ind med {{provider}}", - "vaultAuthTitle": "Login til Vault kræves", - "vaultAuthDescription": "Et vindue er åbnet, hvor du kan logge ind på HashiCorp Vault. Fuldfør loginprocessen der; forbindelsen fortsætter automatisk.", - "vaultAuthFailed": "Godkendelse af arkiv mislykkedes. Prøv igen.", - "vaultReopen": "Genåbn loginvinduet", "sudoPasswordPopupTitle": "Indsæt adgangskode?", - "linkDialogTitle": "Åbn link", - "linkDialogOpen": "Åben", - "linkDialogCopy": "Kopi", - "websocketAbnormalClose": "Forbindelsen blev lukket uventet. Dette kan skyldes et problem med reverse proxy eller SSL-konfigurationen. Tjek venligst serverlogfilerne.", "connectionLogTitle": "Forbindelseslog", "connectionLogCopy": "Kopiér logfiler til udklipsholder", "connectionLogEmpty": "Ingen forbindelseslogfiler endnu", "connectionLogWaiting": "Venter på forbindelseslogfiler...", "connectionLogCopied": "Forbindelseslogfiler kopieret til udklipsholder", "connectionLogCopyFailed": "Kunne ikke kopiere logfiler til udklipsholderen", - "connectionRejected": "Forbindelsen blev afvist af serveren. Kontroller din godkendelse og netværkskonfiguration.", - "hostKeyRejected": "SSH-værtsnøglebekræftelse afvist. Forbindelse afbrudt.", - "sessionTakenOver": "Sessionen blev åbnet i en anden fane. Genopretter forbindelse...", "split": { "splitTab": "Opdel faneblad", "addToSplit": "Tilføj til Opdeling", @@ -2225,55 +2161,6 @@ "sectionApiKeys": "API-nøgler", "sectionAuditLog": "Revisionslog", "sectionSsl": "SSL / Lad os kryptere", - "sectionTouchInput": "Berøringsinput", - "touchEnabled": "Aktivér berøringsinput", - "touchEnabledDesc": "Tillad scrolling med én finger i terminaler", - "touchMomentumEnabled": "Aktiver momentum", - "touchMomentumEnabledDesc": "Fortsæt med at scrolle kort efter et swipe", - "touchAdvanced": "Fremskreden", - "touchDragThreshold": "Trækgrænse", - "touchMaxWheelDelta": "Maksimal hjuldelta", - "touchMomentumSampleWindow": "Momentum-eksempelvindue", - "touchReleaseGracePeriod": "Frigivelsesperiode", - "touchMinimumVelocity": "Minimumshastighed", - "touchMaximumVelocity": "Maksimal hastighed", - "touchMaximumDuration": "Maksimal varighed", - "touchMaximumTravel": "Maksimal rejse", - "touchDecayTime": "Henfaldstid", - "touchPixelsPerTick": "Pixels pr. tick", - "touchMaximumFrameInterval": "Maksimalt billedinterval", - "touchMaximumTicksPerFrame": "Maksimalt antal ticks pr. frame", - "touchResetDefaults": "Nulstil til standardindstillinger", - "touchSaved": "Indstillinger for berøringsinput gemt", - "touchSaveFailed": "Indstillinger for berøringsinput kunne ikke gemmes", - "sectionImageStorage": "Billedlagring", - "imageStorageMode": "Lagringstilstand", - "imageStorageModeDesc": "Hvor indsatte og uploadede terminalbilleder skrives. Automatiske valg efter forbindelse, lokal og fjern (SFTP) er rettet.", - "imageStorageModeAuto": "Bil", - "imageStorageModeLocal": "Lokal", - "imageStorageModeRemoteSftp": "Fjernbetjening (SFTP)", - "imageStorageLocalDir": "Lokal mappe", - "imageStorageLocalDirDesc": "Absolut sti på Termix-backend. Vises ikke efter lagring af sikkerhedsmæssige årsager.", - "imageStorageLocalDirPlaceholder": "Lad stå tomt for at holde det opdateret", - "imageStorageHostPath": "Værtssti", - "imageStorageHostPathDesc": "Værtsvisningen af den lokale mappe, som terminalagenter modtager.", - "imageStorageTtl": "TTL (ms)", - "imageStorageMaxCount": "Maksimalt antal billeder", - "imageStorageMaxBytes": "Maks. lagerplads i bytes", - "imageStorageSaved": "Indstillinger for billedlagring gemt", - "imageStorageSaveFailed": "Kunne ikke gemme indstillinger for billedlagring", - "imageStorageInstanceId": "Terminalsessionsinstans-ID", - "imageStorageInstanceIdDesc": "Instans-ID for en af dine tilsluttede terminalfaner, der bruges til at teste synligheden.", - "imageStorageInstanceIdRequired": "Indtast først et terminalsessionsinstans-ID", - "imageStorageTest": "Prøve", - "imageStorageTesting": "Testning...", - "imageStorageTestFailed": "Kunne ikke teste billedlagring", - "imageStorageTestConnected": "Terminalsessionen er forbundet.", - "imageStorageTestNotConnected": "Ingen forbundet terminalsession med dette instans-ID.", - "imageStorageTestSelectedMode": "En upload ville bruge: {{mode}}", - "imageStorageSelectedModeUnavailable": "Ikke tilgængelig", - "imageStorageTestLocalVisible": "Den lokale mappe er synlig fra den tilsluttede vært.", - "imageStorageTestLocalNotVisible": "Den lokale mappe er IKKE synlig fra den tilsluttede vært.", "sectionBranding": "Hvid etiket", "brandingAppName": "Applikationsnavn", "brandingAppNameDesc": "Vises på loginskærmen og i browserfanen. Lad feltet stå tomt for at bruge \"Termix\".", @@ -2354,9 +2241,6 @@ "oidcSilentLoginDefaultLockedDesc": "Denne indstilling er låst af miljøvariablen OIDC_SILENT_LOGIN_DEFAULT og kan ikke ændres her.", "allowPasswordReset": "Tillad nulstilling af adgangskode", "allowPasswordResetDesc": "Nulstil kode via Docker-logfiler", - "commandHistoryEnabled": "Kommandohistorik", - "commandHistoryEnabledDesc": "Tillad optagelse af kommandohistorik. Når den er deaktiveret, gemmes historikken ikke uanset indstillingerne for den enkelte vært.", - "updateCommandHistoryFailed": "Kunne ikke opdatere indstillingen for kommandohistorik", "analyticsEnabled": "Del anonym brugsstatistik", "analyticsEnabledDesc": "Sender en anonym daglig optælling af brugere, værter og funktionsbrug for at forbedre Termix. Ingen personlige data eller forbindelsesoplysninger inkluderes nogensinde.", "analyticsEnabledLockedDesc": "Denne indstilling er låst af miljøvariablen ENABLE_TELEMETRY og kan ikke ændres her.", @@ -2647,11 +2531,6 @@ "hostDefaultsTerminal": "Terminal", "hostDefaultsAutoTmux": "Auto-Tmux", "hostDefaultsAutoTmuxDesc": "Kør nye værters terminaler inde i tmux, så sessioner overlever afbrydelser (kræver tmux på værten)", - "terminalSessionTimeout": "Terminalsessionens persistens", - "terminalSessionTimeoutDesc": "Hvor længe en frakoblet terminal holdes aktiv på serveren, før den lukkes. Min. 1 · Maks. 1440 minutter. For sessioner, der skal overleve dette, skal du aktivere Auto-Tmux på værten.", - "terminalSessionTimeoutRange": "Indtast en værdi mellem 1 og 1440 minutter", - "terminalSessionTimeoutSaved": "Terminalsessionens persistens gemt", - "terminalSessionTimeoutSaveFailed": "Kunne ikke gemme terminalsessionens persistens", "minutes": "minutter", "hostDefaultsSessionLogging": "Sessionslogning", "hostDefaultsSessionLoggingDesc": "Optag terminalsessioner på nye værter som standard", @@ -2708,15 +2587,6 @@ "connectToTerminal": "Opret forbindelse til terminalen", "connectToFiles": "Opret forbindelse til filer" }, - "history": { - "noTerminalSelected": "Ingen terminal valgt", - "noTerminalSelectedHint": "Åbn en SSH-terminalfane for at se dens kommandohistorik", - "searchPlaceholder": "Søgehistorik...", - "clearAll": "Ryd alt", - "noHistoryEntries": "Ingen historikposter", - "trackingDisabled": "Historiksporing er deaktiveret", - "trackingDisabledHint": "Aktivér det i værtens terminalindstillinger." - }, "sshTools": { "keyRecordingTitle": "Nøgleoptagelse", "recordToTerminals": "Optag til terminaler", @@ -3178,30 +3048,11 @@ "terminalToolbar": { "title": "Terminalværktøjslinje", "showActions": "Vis værktøjslinje", - "hideToolbar": "Skjul værktøjslinjen", - "showToolbar": "Udvid værktøjslinjen", - "moveToolbar": "Flyt værktøjslinjen", - "moveToolbarHint": "Træk for at flytte værktøjslinjen", - "layout": "Værktøjslinjevisningstilstand", - "layoutIcon": "Kun ikon", - "layoutLabeled": "Ikoner og etiketter", - "layoutExpanded": "Udvidet med metrikker", - "image": "Billedhandlinger (kun billeder)", - "upload": "Upload billede", - "paste": "Indsæt billede", - "detachTmux": "Afmonter tmux", - "detachTmuxDescription": "Afbryd tmux-session", - "imageActionFailed": "Billedhandling mislykkedes", - "retry": "Prøv igen", - "dismissError": "Afvis billedfejl", "cpu": "CPU", "memory": "Hukommelse", "disk": "Disk", "statsUnavailable": "Livestatistik ikke tilgængelig", "tmuxDetach": "Afbryd tmux-session", - "uploadImage": "Upload et billede til værten", - "uploadingImage": "Uploader...", - "pasteImage": "Indsæt billede fra udklipsholder (kun billeder)", "openFiles": "Åbne filer", "openDocker": "Åbn Docker", "openTunnel": "Åben tunnel", @@ -3285,31 +3136,6 @@ "timeHours": "For {{count}}timer siden", "timeDays": "for {{count}}dage siden" }, - "mobileKeyboard": { - "shift": "Flytte", - "ctrl": "Ctrl", - "esc": "Esc", - "tab": "Faneblad", - "backTab": "⇥", - "arrowUp": "Pil op", - "arrowDown": "Pil ned", - "arrowLeft": "Pil til venstre", - "arrowRight": "Pil til højre", - "home": "Hjem", - "end": "Ende", - "pageUp": "Side op", - "pageDown": "PgDn", - "delete": "Del", - "paste": "Indsæt", - "editQuickKeys": "Rediger genvejstaster", - "quickKeysTitle": "Hurtigtaster", - "quickKeysDesc": "Tryk på × for at fjerne. Understøtter op til 8 tegn.", - "quickKeyPlaceholder": "f.eks. sudo ", - "addQuickKey": "Tilføje", - "removeQuickKey": "Fjerne", - "resetDefaults": "Nulstil til standardindstillinger", - "done": "Færdig" - }, "serial": { "title": "Seriel", "portLabel": "Havn", diff --git a/src/ui/locales/translated/de_DE.json b/src/ui/locales/translated/de_DE.json index 5f23444dc..bd321f607 100644 --- a/src/ui/locales/translated/de_DE.json +++ b/src/ui/locales/translated/de_DE.json @@ -742,7 +742,6 @@ "addHost": "Host hinzufügen", "editHost": "Host bearbeiten", "cloneHost": "Host klonen", - "enableTerminal": "Terminal aktivieren", "enableTunnel": "Tunnel aktivieren", "enableFileManager": "Dateimanager aktivieren", "enableDocker": "Docker aktivieren", @@ -1041,8 +1040,6 @@ "enableSessionLoggingDesc": "Terminal-Sitzungsausgabe zur späteren Überprüfung aufzeichnen", "allowSessionSharing": "Sitzungsfreigabe zulassen", "allowSessionSharingDesc": "Ermöglichen Sie es, Live-Sitzungen auf diesem Host per Link oder mit anderen Benutzern zu teilen.", - "enableCommandHistory": "Befehlsverlauf", - "enableCommandHistoryDesc": "In diesem Terminal ausgeführte Befehle für Verlauf und Autovervollständigung aufzeichnen", "localEchoLabel": "Lokales Echo", "localEchoDesc": "Bei langsamen Verbindungen werden sichere, druckbare Tastatureingaben lokal vorhergesagt. Passwortabfragen und Steuereingaben werden niemals wiederholt.", "localEchoDefault": "Globale Einstellung verwenden", @@ -1072,8 +1069,6 @@ "bindHost": "Bind-Host", "enableTmuxMonitor": "Tmux-Monitor aktivieren", "enableTmuxMonitorDesc": "Diesen Host im Tmux-Monitor anzeigen und seine tmux-Aktionen zur Seitenleiste hinzufügen", - "enableTerminalToolbar": "Terminal-Symbolleiste aktivieren", - "enableTerminalToolbarDesc": "Zeigt eine Schnellzugriffsleiste an, die am aktiven Terminal angedockt ist und die Tools und Live-Statistiken dieses Hosts anzeigt.", "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.", @@ -1550,9 +1545,7 @@ "links": "Links", "navigate": "Navigieren", "select": "Auswählen", - "toggleWith": "Umschalten mit", - "localTerminal": "Lokales Terminal", - "localTerminalDesc": "Öffnen Sie eine Shell auf diesem Computer" + "toggleWith": "Umschalten mit" }, "splitScreen": { "paneEmpty": "Bereich {{index}} - leer", @@ -1717,57 +1710,13 @@ "loadSharesFailed": "Fehler beim Laden der aktiven Anteile" }, "terminal": { - "sessionExpiredNotice": "Ihre vorherige Sitzung auf {{host}} ist nach {{minutes}} Minuten Verbindungsabbruch abgelaufen; die Shell und alle darin laufenden Prozesse wurden gelöscht. Aktivieren Sie Auto-Tmux auf diesem Host, um Sitzungen aufrechtzuerhalten.", - "sessionExpiredNoticeNoMinutes": "Ihre vorherige Sitzung auf {{host}} ist während der Trennung abgelaufen; die Shell und alle darin laufenden Prozesse wurden gelöscht. Aktivieren Sie Auto-Tmux auf diesem Host, um Sitzungen aufrechtzuerhalten.", - "enableAutoTmuxAction": "Auto-Tmux aktivieren", - "autoTmuxEnabled": "Auto-Tmux ist für {{host}}aktiviert. Es gilt ab der nächsten Verbindung.", - "autoTmuxEnableFailed": "Auto-Tmux konnte nicht aktiviert werden.", "connect": "Mit Host verbinden", "clear": "Löschen", "paste": "Einfügen", - "reconnect": "Erneut verbinden", - "connectionLost": "Verbindung verloren", "connected": "Verbunden", "clipboardWriteFailed": "Kopieren in die Zwischenablage fehlgeschlagen. Stellen Sie sicher, dass die Seite über HTTPS oder localhost bereitgestellt wird.", "clipboardReadFailed": "Lesen aus der Zwischenablage fehlgeschlagen. Stellen Sie sicher, dass die Zwischenablageberechtigungen erteilt wurden.", - "clipboardHttpWarning": "Die Zwischenablage konnte nicht gelesen werden. Einige Browser beschränken den Zugriff auf die Zwischenablage außerhalb von HTTPS – versuchen Sie stattdessen Strg+V oder verwenden Sie für Termix eine HTTPS-Verbindung.", - "passwordPromptFillTitle": "Gespeichertes Passwort in diese Eingabeaufforderung einfügen?", - "unknownError": "Unbekannter Fehler aufgetreten", - "websocketError": "WebSocket-Verbindungsfehler", - "connecting": "Verbinde...", - "noHostSelected": "Kein Host ausgewählt", - "reconnecting": "Wiederverbinden... ({{attempt}}/{{max}})", - "reconnected": "Erfolgreich wieder verbunden", - "tmuxSessionCreated": "tmux-Sitzung erstellt: {{name}}", - "tmuxSessionAttached": "tmux-Sitzung verbunden: {{name}}", - "tmuxUnavailable": "tmux ist auf dem Remote-Host nicht installiert, Fallback auf die Standard-Shell", - "tmuxSessionPickerTitle": "tmux-Sitzungen", - "tmuxSessionPickerDesc": "Bestehende tmux-Sitzungen auf diesem Host gefunden. Wählen Sie eine aus, um sie erneut zu verbinden, oder erstellen Sie eine neue.", - "tmuxWindows": "Fenster", - "tmuxWindowCount": "{{count}} Fenster", - "tmuxAttached": "Verbundene Clients", - "tmuxAttachedCount": "{{count}} verbunden", - "tmuxLastActivity": "Letzte Aktivität", - "tmuxTimeJustNow": "Soeben", - "tmuxTimeMinutes": "vor {{count}} Min.", - "tmuxTimeHours": "vor {{count}} Std.", - "tmuxTimeDays": "vor {{count}} Tagen", - "tmuxCreateNew": "Neue Sitzung starten", - "tmuxCopyHint": "Auswahl anpassen und Enter drücken, um in die Zwischenablage zu kopieren", "tmuxDetach": "Von tmux-Sitzung trennen", - "tmuxDetached": "Von tmux-Sitzung getrennt", - "searchPlaceholder": "Finden", - "searchCaseSensitive": "Streichholzschachtel", - "searchWholeWord": "Gleiches Wort aus", - "searchRegex": "Regulären Ausdruck verwenden", - "searchNoResults": "Keine Ergebnisse", - "searchResultCount": "{{index}} von {{count}}", - "searchNext": "Nächstes Spiel (Eingabe)", - "searchPrevious": "Vorheriges Spiel (Umschalt+Eingabe)", - "searchClose": "Schließen (Flucht)", - "maxReconnectAttemptsReached": "Maximale Wiederverbindungsversuche erreicht", - "closeTab": "Schließen", - "connectionTimeout": "Verbindungs-Timeout", "terminalTitle": "Terminal - {{host}}", "terminalWithPath": "Terminal - {{host}}:{{path}}", "runTitle": "Ausführung: {{command}} - {{host}}", @@ -1790,27 +1739,14 @@ "opksshOpenBrowser": "Browser zum Authentifizieren öffnen", "opksshWaitingForAuth": "Warten auf Authentifizierung im Browser...", "opksshAuthenticating": "Authentifizierung wird verarbeitet...", - "opksshTimeout": "Authentifizierung abgelaufen. Bitte versuchen Sie es erneut.", - "opksshAuthFailed": "Authentifizierung fehlgeschlagen. Bitte überprüfen Sie Ihre Anmeldeinformationen und versuchen Sie es erneut.", "opksshSignInWith": "Anmelden mit {{provider}}", - "vaultAuthTitle": "Vault-Anmeldung erforderlich", - "vaultAuthDescription": "Ein Fenster zur Anmeldung bei HashiCorp Vault wurde geöffnet. Schließen Sie die Anmeldung dort ab; diese Verbindung wird automatisch fortgesetzt.", - "vaultAuthFailed": "Vault-Authentifizierung fehlgeschlagen. Bitte versuchen Sie es erneut.", - "vaultReopen": "Anmeldefenster erneut öffnen", "sudoPasswordPopupTitle": "Passwort eingeben?", - "linkDialogTitle": "Link öffnen", - "linkDialogOpen": "Öffnen", - "linkDialogCopy": "Kopieren", - "websocketAbnormalClose": "Verbindung unerwartet geschlossen. Dies kann auf ein Problem mit dem Reverse-Proxy oder der SSL-Konfiguration zurückzuführen sein. Bitte überprüfen Sie die Server-Logs.", "connectionLogTitle": "Verbindungsprotokoll", "connectionLogCopy": "Protokolle in die Zwischenablage kopieren", "connectionLogEmpty": "Noch keine Verbindungsprotokolle", "connectionLogWaiting": "Warten auf Verbindungsprotokolle...", "connectionLogCopied": "Verbindungsprotokolle in die Zwischenablage kopiert", "connectionLogCopyFailed": "Fehler beim Kopieren der Protokolle in die Zwischenablage", - "connectionRejected": "Verbindung vom Server abgelehnt. Bitte überprüfen Sie Ihre Authentifizierung und Netzwerkkonfiguration.", - "hostKeyRejected": "SSH-Hostschlüssel-Überprüfung abgelehnt. Verbindung abgebrochen.", - "sessionTakenOver": "Sitzung wurde in einem anderen Tab geöffnet. Verbindung wird wiederhergestellt...", "split": { "splitTab": "Tab teilen", "addToSplit": "Zur Teilung hinzufügen", @@ -2225,55 +2161,6 @@ "sectionApiKeys": "API-Schlüssel", "sectionAuditLog": "Audit-Log", "sectionSsl": "SSL / Let's Encrypt", - "sectionTouchInput": "Berührungseingabe", - "touchEnabled": "Touch-Eingabe aktivieren", - "touchEnabledDesc": "Ein-Finger-Touch-Scrollen in Terminals ermöglichen", - "touchMomentumEnabled": "Dynamik aktivieren", - "touchMomentumEnabledDesc": "Nach dem Wischen kurz weiterscrollen.", - "touchAdvanced": "Fortschrittlich", - "touchDragThreshold": "Widerstandsschwelle", - "touchMaxWheelDelta": "Maximale Rad-Delta", - "touchMomentumSampleWindow": "Momentum-Abtastfenster", - "touchReleaseGracePeriod": "Freigabefrist", - "touchMinimumVelocity": "Minimale Geschwindigkeit", - "touchMaximumVelocity": "Maximale Geschwindigkeit", - "touchMaximumDuration": "Maximale Dauer", - "touchMaximumTravel": "Maximale Reisegeschwindigkeit", - "touchDecayTime": "Zerfallszeit", - "touchPixelsPerTick": "Pixel pro Tick", - "touchMaximumFrameInterval": "Maximales Frame-Intervall", - "touchMaximumTicksPerFrame": "Maximale Anzahl an Ticks pro Frame", - "touchResetDefaults": "Auf Standardeinstellungen zurücksetzen", - "touchSaved": "Touch-Eingabeeinstellungen gespeichert", - "touchSaveFailed": "Speichern der Touch-Eingabeeinstellungen fehlgeschlagen", - "sectionImageStorage": "Bildspeicherung", - "imageStorageMode": "Speichermodus", - "imageStorageModeDesc": "Hier werden eingefügte und hochgeladene Terminalbilder gespeichert. Die automatische Auswahl anhand der Verbindung (lokal und remote (SFTP)) ist festgelegt.", - "imageStorageModeAuto": "Auto", - "imageStorageModeLocal": "Lokal", - "imageStorageModeRemoteSftp": "Remote (SFTP)", - "imageStorageLocalDir": "Lokales Verzeichnis", - "imageStorageLocalDirDesc": "Absoluter Pfad im Termix-Backend. Aus Sicherheitsgründen nach dem Speichern nicht angezeigt.", - "imageStorageLocalDirPlaceholder": "Lassen Sie dieses Feld leer, um den aktuellen Stand beizubehalten.", - "imageStorageHostPath": "Hostpfad", - "imageStorageHostPathDesc": "Die hostseitige Ansicht des lokalen Verzeichnisses, die Terminalagenten empfangen.", - "imageStorageTtl": "TTL (ms)", - "imageStorageMaxCount": "Maximale Bildanzahl", - "imageStorageMaxBytes": "Maximale Speicherkapazität in Bytes", - "imageStorageSaved": "Bildspeichereinstellungen gespeichert", - "imageStorageSaveFailed": "Bildspeichereinstellungen konnten nicht gespeichert werden", - "imageStorageInstanceId": "Terminal-Sitzungsinstanz-ID", - "imageStorageInstanceIdDesc": "Instanz-ID eines Ihrer verbundenen Terminal-Tabs, die zum Testen der Sichtbarkeit verwendet wird.", - "imageStorageInstanceIdRequired": "Geben Sie zuerst eine Terminal-Sitzungsinstanz-ID ein.", - "imageStorageTest": "Prüfen", - "imageStorageTesting": "Testen...", - "imageStorageTestFailed": "Fehler beim Testen der Bildspeicherung", - "imageStorageTestConnected": "Die Terminal-Sitzung ist verbunden.", - "imageStorageTestNotConnected": "Es besteht keine verbundene Terminal-Sitzung mit dieser Instanz-ID.", - "imageStorageTestSelectedMode": "Ein Upload würde Folgendes verwenden: {{mode}}", - "imageStorageSelectedModeUnavailable": "Nicht verfügbar", - "imageStorageTestLocalVisible": "Das lokale Verzeichnis ist vom verbundenen Host aus sichtbar.", - "imageStorageTestLocalNotVisible": "Das lokale Verzeichnis ist vom verbundenen Host aus nicht sichtbar.", "sectionBranding": "White Label", "brandingAppName": "Anwendungsname", "brandingAppNameDesc": "Wird auf dem Anmeldebildschirm und im Browsertab angezeigt. Lassen Sie dieses Feld leer, um „Termix“ zu verwenden.", @@ -2354,9 +2241,6 @@ "oidcSilentLoginDefaultLockedDesc": "Diese Einstellung ist durch die Umgebungsvariable OIDC_SILENT_LOGIN_DEFAULT gesperrt und kann hier nicht geändert werden.", "allowPasswordReset": "Passwort-Zurücksetzen erlauben", "allowPasswordResetDesc": "Reset-Code über Docker-Logs", - "commandHistoryEnabled": "Befehlsverlauf", - "commandHistoryEnabledDesc": "Aufzeichnung des Befehlsverlaufs erlauben. Wenn deaktiviert, wird der Verlauf unabhängig von Host-Einstellungen nicht gespeichert.", - "updateCommandHistoryFailed": "Fehler beim Aktualisieren der Einstellung für den Befehlsverlauf", "analyticsEnabled": "Anonyme Nutzungsstatistiken teilen", "analyticsEnabledDesc": "Sendet täglich anonyme Zählungen von Nutzern, Hosts und Funktionsnutzung, um Termix zu verbessern. Es werden niemals personenbezogene Daten oder Verbindungsdaten übermittelt.", "analyticsEnabledLockedDesc": "Diese Einstellung ist durch die Umgebungsvariable ENABLE_TELEMETRY gesperrt und kann hier nicht geändert werden.", @@ -2647,11 +2531,6 @@ "hostDefaultsTerminal": "Terminal", "hostDefaultsAutoTmux": "Auto-Tmux", "hostDefaultsAutoTmuxDesc": "Neue Host-Terminals müssen innerhalb von tmux ausgeführt werden, damit die Sitzungen auch bei Verbindungsabbrüchen erhalten bleiben (tmux muss auf dem Host installiert sein).", - "terminalSessionTimeout": "Terminal-Sitzungspersistenz", - "terminalSessionTimeoutDesc": "Wie lange ein getrenntes Terminal auf dem Server aktiv bleibt, bevor es geschlossen wird. Min. 1 Minute · Max. 1440 Minuten. Für Sitzungen, die länger als diese Zeitspanne bestehen bleiben sollen, aktivieren Sie Auto-Tmux auf dem Host.", - "terminalSessionTimeoutRange": "Geben Sie einen Wert zwischen 1 und 1440 Minuten ein.", - "terminalSessionTimeoutSaved": "Terminalsitzungspersistenz gespeichert", - "terminalSessionTimeoutSaveFailed": "Die Speicherung der Terminal-Sitzungspersistenz ist fehlgeschlagen.", "minutes": "Minuten", "hostDefaultsSessionLogging": "Sitzungsprotokollierung", "hostDefaultsSessionLoggingDesc": "Terminalsitzungen standardmäßig auf neuen Hosts aufzeichnen", @@ -2708,15 +2587,6 @@ "connectToTerminal": "Mit Terminal verbinden", "connectToFiles": "Mit Dateien verbinden" }, - "history": { - "noTerminalSelected": "Kein Terminal ausgewählt", - "noTerminalSelectedHint": "Öffnen Sie einen SSH-Terminal-Tab, um den Befehlsverlauf anzuzeigen.", - "searchPlaceholder": "Verlauf durchsuchen...", - "clearAll": "Alle löschen", - "noHistoryEntries": "Keine Verlaufseinträge", - "trackingDisabled": "Verlaufsaufzeichnung ist deaktiviert", - "trackingDisabledHint": "Aktivieren Sie sie in den Terminal-Einstellungen des Hosts." - }, "sshTools": { "keyRecordingTitle": "Tastenaufzeichnung", "recordToTerminals": "Auf Terminals aufzeichnen", @@ -3178,30 +3048,11 @@ "terminalToolbar": { "title": "Terminal-Symbolleiste", "showActions": "Symbolleiste anzeigen", - "hideToolbar": "Werkzeugleiste ausblenden", - "showToolbar": "Symbolleiste erweitern", - "moveToolbar": "Verschieben Sie die Werkzeugleiste", - "moveToolbarHint": "Ziehen Sie, um die Symbolleiste neu zu positionieren.", - "layout": "Symbolleistenanzeigemodus", - "layoutIcon": "Nur Symbol", - "layoutLabeled": "Symbole und Beschriftungen", - "layoutExpanded": "Erweitert um Kennzahlen", - "image": "Bildaktionen (nur Bilder)", - "upload": "Bild hochladen", - "paste": "Bild einfügen", - "detachTmux": "tmux trennen", - "detachTmuxDescription": "tmux-Sitzung trennen", - "imageActionFailed": "Bildaktion fehlgeschlagen", - "retry": "Wiederholen", - "dismissError": "Bildfehler ignorieren", "cpu": "CPU", "memory": "Erinnerung", "disk": "Scheibe", "statsUnavailable": "Live-Statistiken nicht verfügbar", "tmuxDetach": "tmux-Sitzung trennen", - "uploadImage": "Laden Sie ein Bild auf den Host hoch.", - "uploadingImage": "Wird hochgeladen...", - "pasteImage": "Bild aus der Zwischenablage einfügen (nur Bilder)", "openFiles": "Dateien öffnen", "openDocker": "Docker öffnen", "openTunnel": "Offener Tunnel", @@ -3285,31 +3136,6 @@ "timeHours": "vor {{count}} Std.", "timeDays": "vor {{count}} T." }, - "mobileKeyboard": { - "shift": "Umschalt", - "ctrl": "Strg", - "esc": "Esc", - "tab": "Tab", - "backTab": "⇥", - "arrowUp": "Pfeil nach oben", - "arrowDown": "Pfeil nach unten", - "arrowLeft": "Pfeil nach links", - "arrowRight": "Pfeil nach rechts", - "home": "Pos1", - "end": "Ende", - "pageUp": "Bild auf", - "pageDown": "Bild ab", - "delete": "Entf", - "paste": "Paste", - "editQuickKeys": "Schnelltasten bearbeiten", - "quickKeysTitle": "Schnelltasten", - "quickKeysDesc": "Tippen Sie auf × zum Entfernen. Unterstützt bis zu 8 Zeichen.", - "quickKeyPlaceholder": "z. B. sudo ", - "addQuickKey": "Hinzufügen", - "removeQuickKey": "Entfernen", - "resetDefaults": "Auf Standard zurücksetzen", - "done": "Fertig" - }, "serial": { "title": "Seriell", "portLabel": "Port", diff --git a/src/ui/locales/translated/el_GR.json b/src/ui/locales/translated/el_GR.json index 1cd0b67bc..7bdb90ad1 100644 --- a/src/ui/locales/translated/el_GR.json +++ b/src/ui/locales/translated/el_GR.json @@ -742,7 +742,6 @@ "addHost": "Προσθήκη κεντρικού υπολογιστή", "editHost": "Επεξεργασία κεντρικού υπολογιστή", "cloneHost": "Κλωνοποίηση κεντρικού υπολογιστή", - "enableTerminal": "Ενεργοποίηση τερματικού", "enableTunnel": "Ενεργοποίηση σήραγγας", "enableFileManager": "Ενεργοποίηση Διαχείρισης Αρχείων", "enableDocker": "Ενεργοποίηση Docker", @@ -1041,8 +1040,6 @@ "enableSessionLoggingDesc": "Καταγραφή εξόδου συνεδρίας τερματικού για μελλοντική αναθεώρηση", "allowSessionSharing": "Επιτρέψτε την κοινή χρήση περιόδου σύνδεσης", "allowSessionSharingDesc": "Να επιτρέπεται η κοινοποίηση ζωντανών συνεδριών σε αυτόν τον πάροχο μέσω συνδέσμου ή με άλλους χρήστες", - "enableCommandHistory": "Ιστορικό εντολών", - "enableCommandHistoryDesc": "Οι εντολές εγγραφής εκτελούνται σε αυτό το τερματικό για ιστορικό και αυτόματη συμπλήρωση", "localEchoLabel": "Τοπική Ηχώ", "localEchoDesc": "Προβλέψτε ασφαλείς εκτυπώσιμες πληκτρολογήσεις τοπικά σε αργές συνδέσεις. Οι προτροπές κωδικού πρόσβασης και η εισαγωγή ελέγχου δεν επαναλαμβάνονται ποτέ.", "localEchoDefault": "Χρήση καθολικής ρύθμισης", @@ -1072,8 +1069,6 @@ "bindHost": "Bind Host", "enableTmuxMonitor": "Ενεργοποίηση παρακολούθησης Tmux", "enableTmuxMonitorDesc": "Εμφάνιση αυτού του κεντρικού υπολογιστή στην οθόνη Tmux και προσθήκη των ενεργειών tmux του στην πλαϊνή μπάρα", - "enableTerminalToolbar": "Ενεργοποίηση γραμμής εργαλείων τερματικού", - "enableTerminalToolbarDesc": "Εμφάνιση μιας γραμμής εργαλείων γρήγορης πρόσβασης συνδεδεμένης στο ενεργό τερματικό για τα εργαλεία και τα ζωντανά στατιστικά αυτού του κεντρικού υπολογιστή", "connectionToolbar": "Γραμμή εργαλείων σύνδεσης", "showConnectionToolbar": "Εμφάνιση γραμμής εργαλείων σύνδεσης", "showConnectionToolbarDesc": "Εμφάνιση της γραμμής εργαλείων στις συνεδρίες SSH, RDP, VNC και Telnet αυτού του κεντρικού υπολογιστή. Μπορείτε επίσης να την αποκρύψετε προσωρινά από την ίδια τη γραμμή εργαλείων.", @@ -1550,9 +1545,7 @@ "links": "Εδαφος διά παιγνίδι γκολφ", "navigate": "Διαπλέω", "select": "Επιλέγω", - "toggleWith": "Εναλλαγή με", - "localTerminal": "Τοπικό τερματικό", - "localTerminalDesc": "Άνοιγμα κελύφους σε αυτόν τον υπολογιστή" + "toggleWith": "Εναλλαγή με" }, "splitScreen": { "paneEmpty": "Παράθυρο {{index}} - κενό", @@ -1717,57 +1710,13 @@ "loadSharesFailed": "Αποτυχία φόρτωσης ενεργών κοινόχρηστων στοιχείων" }, "terminal": { - "sessionExpiredNotice": "Η προηγούμενη συνεδρία σας στο {{host}} έληξε μετά από {{minutes}} λεπτά αποσύνδεσης. Το κέλυφος και οτιδήποτε εκτελείται σε αυτό έχουν εξαφανιστεί. Ενεργοποιήστε το Auto-Tmux σε αυτόν τον κεντρικό υπολογιστή για να διατηρήσετε ενεργές τις συνεδρίες.", - "sessionExpiredNoticeNoMinutes": "Η προηγούμενη συνεδρία σας στο {{host}} έληξε κατά την αποσύνδεση. Το κέλυφος και οτιδήποτε εκτελείται σε αυτό έχουν χαθεί. Ενεργοποιήστε το Auto-Tmux σε αυτόν τον κεντρικό υπολογιστή για να διατηρήσετε ενεργές τις συνεδρίες.", - "enableAutoTmuxAction": "Ενεργοποίηση Auto-Tmux", - "autoTmuxEnabled": "Το Auto-Tmux είναι ενεργοποιημένο για {{host}}. Εφαρμόζεται από την επόμενη σύνδεση.", - "autoTmuxEnableFailed": "Αποτυχία ενεργοποίησης του Auto-Tmux", "connect": "Σύνδεση με τον κεντρικό υπολογιστή", "clear": "Σαφής", "paste": "Πάστα", - "reconnect": "Επανασύνδεση", - "connectionLost": "Η σύνδεση χάθηκε", "connected": "Συνδεδεμένος", "clipboardWriteFailed": "Αποτυχία αντιγραφής στο πρόχειρο. Βεβαιωθείτε ότι η σελίδα προβάλλεται μέσω HTTPS ή localhost.", "clipboardReadFailed": "Αποτυχία ανάγνωσης από το πρόχειρο. Βεβαιωθείτε ότι έχουν εκχωρηθεί δικαιώματα στο πρόχειρο.", - "clipboardHttpWarning": "Δεν ήταν δυνατή η ανάγνωση του πρόχειρου. Ορισμένα προγράμματα περιήγησης περιορίζουν την πρόσβαση στο πρόχειρο εκτός HTTPS - δοκιμάστε Ctrl+V ή προβάλετε το Termix μέσω HTTPS.", - "passwordPromptFillTitle": "Συμπληρώστε τον αποθηκευμένο κωδικό πρόσβασης σε αυτήν την ερώτηση;", - "unknownError": "Παρουσιάστηκε άγνωστο σφάλμα", - "websocketError": "Σφάλμα σύνδεσης WebSocket", - "connecting": "Συνδετικός...", - "noHostSelected": "Δεν έχει επιλεγεί κεντρικός υπολογιστής", - "reconnecting": "Επανασύνδεση... ({{attempt}}/{{max}})", - "reconnected": "Επανασύνδεση με επιτυχία", - "tmuxSessionCreated": "δημιουργήθηκε η συνεδρία tmux: {{name}}", - "tmuxSessionAttached": "συνεδρία tmux επισυνάπτεται: {{name}}", - "tmuxUnavailable": "Το tmux δεν είναι εγκατεστημένο στον απομακρυσμένο κεντρικό υπολογιστή, επιστρέφοντας στο τυπικό κέλυφος", - "tmuxSessionPickerTitle": "Συνεδρίες tmux", - "tmuxSessionPickerDesc": "Βρέθηκαν υπάρχουσες συνεδρίες tmux σε αυτόν τον κεντρικό υπολογιστή. Επιλέξτε μία για να επανασυνδέσετε ή να δημιουργήσετε μια νέα συνεδρία.", - "tmuxWindows": "Παράθυρα", - "tmuxWindowCount": "παράθυρο {{count}}", - "tmuxAttached": "Συνδεδεμένοι πελάτες", - "tmuxAttachedCount": "{{count}} επισυνάπτεται", - "tmuxLastActivity": "Τελευταία δραστηριότητα", - "tmuxTimeJustNow": "μόλις τώρα", - "tmuxTimeMinutes": "{{count}}πριν από λεπτά", - "tmuxTimeHours": "{{count}}πριν από ώρες", - "tmuxTimeDays": "{{count}}πριν από ημέρες", - "tmuxCreateNew": "Έναρξη νέας συνεδρίας", - "tmuxCopyHint": "Προσαρμόστε την επιλογή και πατήστε Enter για αντιγραφή στο πρόχειρο", "tmuxDetach": "Αποσύνδεση από την περίοδο λειτουργίας tmux", - "tmuxDetached": "Αποσπάστηκε από την περίοδο λειτουργίας tmux", - "searchPlaceholder": "Εύρημα", - "searchCaseSensitive": "Ταίριασμα υπόθεσης", - "searchWholeWord": "Ταίριασμα ολόκληρης λέξης", - "searchRegex": "Χρήση κανονικής έκφρασης", - "searchNoResults": "Δεν υπάρχουν αποτελέσματα", - "searchResultCount": "{{index}} από {{count}}", - "searchNext": "Επόμενος αγώνας (Είσοδος)", - "searchPrevious": "Προηγούμενος αγώνας (Shift+Enter)", - "searchClose": "Κλείσιμο (Escape)", - "maxReconnectAttemptsReached": "Επιτεύχθηκε ο μέγιστος αριθμός προσπαθειών επανασύνδεσης", - "closeTab": "Κοντά", - "connectionTimeout": "Λήξη χρονικού ορίου σύνδεσης", "terminalTitle": "Τερματικό - {{host}}", "terminalWithPath": "Τερματικό - {{host}}:{{path}}", "runTitle": "Εκτέλεση {{command}} - {{host}}", @@ -1790,27 +1739,14 @@ "opksshOpenBrowser": "Άνοιγμα προγράμματος περιήγησης για έλεγχο ταυτότητας", "opksshWaitingForAuth": "Αναμονή για έλεγχο ταυτότητας στο πρόγραμμα περιήγησης...", "opksshAuthenticating": "Επεξεργασία ελέγχου ταυτότητας...", - "opksshTimeout": "Το χρονικό όριο ελέγχου ταυτότητας έληξε. Δοκιμάστε ξανά.", - "opksshAuthFailed": "Ο έλεγχος ταυτότητας απέτυχε. Ελέγξτε τα διαπιστευτήριά σας και προσπαθήστε ξανά.", "opksshSignInWith": "Συνδεθείτε με {{provider}}", - "vaultAuthTitle": "Απαιτείται σύνδεση στο Vault", - "vaultAuthDescription": "Έχει ανοίξει ένα παράθυρο για να συνδεθείτε στο HashiCorp Vault. Ολοκληρώστε τη σύνδεση εκεί. Αυτή η σύνδεση θα συνεχιστεί αυτόματα.", - "vaultAuthFailed": "Ο έλεγχος ταυτότητας του vault απέτυχε. Δοκιμάστε ξανά.", - "vaultReopen": "Επανάνοιγμα παραθύρου σύνδεσης", "sudoPasswordPopupTitle": "Εισαγωγή κωδικού πρόσβασης;", - "linkDialogTitle": "Άνοιγμα συνδέσμου", - "linkDialogOpen": "Ανοιχτό", - "linkDialogCopy": "Αντίγραφο", - "websocketAbnormalClose": "Η σύνδεση έκλεισε απροσδόκητα. Αυτό μπορεί να οφείλεται σε πρόβλημα αντίστροφης μεσολάβησης ή ρύθμισης παραμέτρων SSL. Ελέγξτε τα αρχεία καταγραφής διακομιστή.", "connectionLogTitle": "Αρχείο καταγραφής σύνδεσης", "connectionLogCopy": "Αντιγραφή αρχείων καταγραφής στο πρόχειρο", "connectionLogEmpty": "Δεν υπάρχουν ακόμη αρχεία καταγραφής σύνδεσης", "connectionLogWaiting": "Αναμονή για αρχεία καταγραφής σύνδεσης...", "connectionLogCopied": "Τα αρχεία καταγραφής σύνδεσης αντιγράφηκαν στο πρόχειρο", "connectionLogCopyFailed": "Η αντιγραφή των αρχείων καταγραφής στο πρόχειρο απέτυχε", - "connectionRejected": "Η σύνδεση απορρίφθηκε από τον διακομιστή. Ελέγξτε τον έλεγχο ταυτότητας και τη διαμόρφωση δικτύου.", - "hostKeyRejected": "Η επαλήθευση του κλειδιού κεντρικού υπολογιστή SSH απορρίφθηκε. Η σύνδεση ακυρώθηκε.", - "sessionTakenOver": "Η συνεδρία άνοιξε σε άλλη καρτέλα. Επανασύνδεση...", "split": { "splitTab": "Διαίρεση καρτέλας", "addToSplit": "Προσθήκη στο Split", @@ -2225,55 +2161,6 @@ "sectionApiKeys": "Κλειδιά API", "sectionAuditLog": "Αρχείο καταγραφής ελέγχου", "sectionSsl": "SSL / Ας κρυπτογραφήσουμε", - "sectionTouchInput": "Εισαγωγή με άγγιγμα", - "touchEnabled": "Ενεργοποίηση εισαγωγής αφής", - "touchEnabledDesc": "Επιτρέψτε την κύλιση με ένα δάχτυλο σε τερματικά", - "touchMomentumEnabled": "Ενεργοποίηση ορμής", - "touchMomentumEnabledDesc": "Συνεχίστε σύντομα την κύλιση μετά από μια σάρωση", - "touchAdvanced": "Προχωρημένος", - "touchDragThreshold": "Όριο οπισθέλκουσας", - "touchMaxWheelDelta": "Μέγιστο δέλτα τροχού", - "touchMomentumSampleWindow": "Παράθυρο δείγματος ορμής", - "touchReleaseGracePeriod": "Περίοδος χάριτος αποδέσμευσης", - "touchMinimumVelocity": "Ελάχιστη ταχύτητα", - "touchMaximumVelocity": "Μέγιστη ταχύτητα", - "touchMaximumDuration": "Μέγιστη διάρκεια", - "touchMaximumTravel": "Μέγιστη διαδρομή", - "touchDecayTime": "Χρόνος αποσύνθεσης", - "touchPixelsPerTick": "Εικονοστοιχεία ανά τικ", - "touchMaximumFrameInterval": "Μέγιστο διάστημα καρέ", - "touchMaximumTicksPerFrame": "Μέγιστος αριθμός τικ ανά καρέ", - "touchResetDefaults": "Επαναφορά στις προεπιλογές", - "touchSaved": "Οι ρυθμίσεις εισαγωγής αφής αποθηκεύτηκαν", - "touchSaveFailed": "Αποτυχία αποθήκευσης ρυθμίσεων εισαγωγής αφής", - "sectionImageStorage": "Αποθήκευση εικόνων", - "imageStorageMode": "Λειτουργία αποθήκευσης", - "imageStorageModeDesc": "Πού εγγράφονται επικολλημένες και μεταφορτωμένες εικόνες τερματικού. Οι αυτόματες επιλογές ανά σύνδεση, Τοπικές και Απομακρυσμένες (SFTP) έχουν διορθωθεί.", - "imageStorageModeAuto": "Αυτο", - "imageStorageModeLocal": "Τοπικός", - "imageStorageModeRemoteSftp": "Απομακρυσμένο (SFTP)", - "imageStorageLocalDir": "Τοπικός κατάλογος", - "imageStorageLocalDirDesc": "Απόλυτη διαδρομή στο backend του Termix. Δεν εμφανίζεται μετά την αποθήκευση για λόγους ασφαλείας.", - "imageStorageLocalDirPlaceholder": "Αφήστε το κενό για να διατηρήσετε την τρέχουσα τιμή", - "imageStorageHostPath": "Διαδρομή κεντρικού υπολογιστή", - "imageStorageHostPathDesc": "Η προβολή από την πλευρά του κεντρικού υπολογιστή του τοπικού καταλόγου που λαμβάνουν οι πράκτορες τερματικού.", - "imageStorageTtl": "TTL (ms)", - "imageStorageMaxCount": "Μέγιστος αριθμός εικόνων", - "imageStorageMaxBytes": "Μέγιστος αριθμός byte αποθήκευσης", - "imageStorageSaved": "Οι ρυθμίσεις αποθήκευσης εικόνων αποθηκεύτηκαν", - "imageStorageSaveFailed": "Αποτυχία αποθήκευσης ρυθμίσεων αποθήκευσης εικόνων", - "imageStorageInstanceId": "Αναγνωριστικό παρουσίας περιόδου λειτουργίας τερματικού", - "imageStorageInstanceIdDesc": "Αναγνωριστικό παρουσίας μίας από τις συνδεδεμένες καρτέλες τερματικού σας, που χρησιμοποιείται για τον έλεγχο της ορατότητας.", - "imageStorageInstanceIdRequired": "Εισαγάγετε πρώτα ένα αναγνωριστικό παρουσίας περιόδου λειτουργίας τερματικού", - "imageStorageTest": "Δοκιμή", - "imageStorageTesting": "Δοκιμές...", - "imageStorageTestFailed": "Αποτυχία δοκιμής χώρου αποθήκευσης εικόνων", - "imageStorageTestConnected": "Η συνεδρία τερματικού είναι συνδεδεμένη.", - "imageStorageTestNotConnected": "Δεν υπάρχει συνδεδεμένη συνεδρία τερματικού με αυτό το αναγνωριστικό παρουσίας.", - "imageStorageTestSelectedMode": "Μια μεταφόρτωση θα χρησιμοποιούσε: {{mode}}", - "imageStorageSelectedModeUnavailable": "Δυσεύρετος", - "imageStorageTestLocalVisible": "Ο τοπικός κατάλογος είναι ορατός από τον συνδεδεμένο κεντρικό υπολογιστή.", - "imageStorageTestLocalNotVisible": "Ο τοπικός κατάλογος ΔΕΝ είναι ορατός από τον συνδεδεμένο κεντρικό υπολογιστή.", "sectionBranding": "Λευκή ετικέτα", "brandingAppName": "Όνομα εφαρμογής", "brandingAppNameDesc": "Εμφανίζεται στην οθόνη σύνδεσης και στην καρτέλα του προγράμματος περιήγησης. Αφήστε το κενό για να χρησιμοποιήσετε το \"Termix\".", @@ -2354,9 +2241,6 @@ "oidcSilentLoginDefaultLockedDesc": "Αυτή η ρύθμιση είναι κλειδωμένη από τη μεταβλητή περιβάλλοντος OIDC_SILENT_LOGIN_DEFAULT και δεν μπορεί να αλλάξει εδώ.", "allowPasswordReset": "Να επιτρέπεται η επαναφορά κωδικού πρόσβασης", "allowPasswordResetDesc": "Επαναφορά κώδικα μέσω αρχείων καταγραφής Docker", - "commandHistoryEnabled": "Ιστορικό εντολών", - "commandHistoryEnabledDesc": "Να επιτρέπεται η καταγραφή ιστορικού εντολών. Όταν είναι απενεργοποιημένη, το ιστορικό δεν αποθηκεύεται ανεξάρτητα από τις ρυθμίσεις ανά κεντρικό υπολογιστή.", - "updateCommandHistoryFailed": "Αποτυχία ενημέρωσης της ρύθμισης ιστορικού εντολών", "analyticsEnabled": "Κοινοποίηση ανώνυμων στατιστικών χρήσης", "analyticsEnabledDesc": "Αποστέλλει έναν ανώνυμο ημερήσιο αριθμό χρηστών, κεντρικών υπολογιστών και χρήσης λειτουργιών για να βοηθήσει στη βελτίωση του Termix. Δεν περιλαμβάνονται ποτέ προσωπικά δεδομένα ή λεπτομέρειες σύνδεσης.", "analyticsEnabledLockedDesc": "Αυτή η ρύθμιση είναι κλειδωμένη από τη μεταβλητή περιβάλλοντος ENABLE_TELEMETRY και δεν μπορεί να αλλάξει εδώ.", @@ -2647,11 +2531,6 @@ "hostDefaultsTerminal": "Τερματικό", "hostDefaultsAutoTmux": "Αυτόματο Tmux", "hostDefaultsAutoTmuxDesc": "Εκτελέστε τερματικά νέων κεντρικών υπολογιστών μέσα στο tmux, ώστε οι συνεδρίες να επιβιώσουν από αποσυνδέσεις (απαιτείται tmux στον κεντρικό υπολογιστή)", - "terminalSessionTimeout": "Διατήρηση Συνεδρίας Τερματικού", - "terminalSessionTimeoutDesc": "Για πόσο χρονικό διάστημα διατηρείται ενεργό ένα αποσυνδεδεμένο τερματικό στον διακομιστή πριν κλείσει. Ελάχιστο 1 · Μέγιστο 1440 λεπτά. Για συνεδρίες που πρέπει να διαρκέσουν περισσότερο από αυτό, ενεργοποιήστε το Auto-Tmux στον κεντρικό υπολογιστή.", - "terminalSessionTimeoutRange": "Εισαγάγετε μια τιμή μεταξύ 1 και 1440 λεπτών", - "terminalSessionTimeoutSaved": "Η διατήρηση της συνεδρίας τερματικού αποθηκεύτηκε", - "terminalSessionTimeoutSaveFailed": "Αποτυχία αποθήκευσης της μόνιμης συνεδρίας τερματικού", "minutes": "πρακτικά", "hostDefaultsSessionLogging": "Καταγραφή συνεδρίας", "hostDefaultsSessionLoggingDesc": "Εγγραφή συνεδριών τερματικού σε νέους κεντρικούς υπολογιστές από προεπιλογή", @@ -2708,15 +2587,6 @@ "connectToTerminal": "Σύνδεση με τερματικό", "connectToFiles": "Σύνδεση με αρχεία" }, - "history": { - "noTerminalSelected": "Δεν έχει επιλεγεί τερματικό", - "noTerminalSelectedHint": "Ανοίξτε μια καρτέλα τερματικού SSH για να δείτε το ιστορικό εντολών του", - "searchPlaceholder": "Ιστορικό αναζήτησης...", - "clearAll": "Εκκαθάριση όλων", - "noHistoryEntries": "Δεν υπάρχουν καταχωρήσεις ιστορικού", - "trackingDisabled": "Η παρακολούθηση ιστορικού είναι απενεργοποιημένη", - "trackingDisabledHint": "Ενεργοποιήστε το στις ρυθμίσεις τερματικού του κεντρικού υπολογιστή." - }, "sshTools": { "keyRecordingTitle": "Ηχογράφηση κλειδιών", "recordToTerminals": "Εγγραφή σε τερματικά", @@ -3178,30 +3048,11 @@ "terminalToolbar": { "title": "Γραμμή εργαλείων τερματικού", "showActions": "Εμφάνιση γραμμής εργαλείων", - "hideToolbar": "Απόκρυψη γραμμής εργαλείων", - "showToolbar": "Ανάπτυξη γραμμής εργαλείων", - "moveToolbar": "Μετακίνηση γραμμής εργαλείων", - "moveToolbarHint": "Σύρετε για να αλλάξετε τη θέση της γραμμής εργαλείων", - "layout": "Λειτουργία εμφάνισης γραμμής εργαλείων", - "layoutIcon": "Μόνο εικονίδιο", - "layoutLabeled": "Εικονίδια και ετικέτες", - "layoutExpanded": "Επεκτάθηκε με μετρήσεις", - "image": "Ενέργειες εικόνας (μόνο εικόνες)", - "upload": "Μεταφόρτωση εικόνας", - "paste": "Επικόλληση εικόνας", - "detachTmux": "Αποσύνδεση tmux", - "detachTmuxDescription": "Αποσύνδεση συνεδρίας tmux", - "imageActionFailed": "Η ενέργεια εικόνας απέτυχε", - "retry": "Δοκιμάζω πάλι", - "dismissError": "Σφάλμα απόρριψης εικόνας", "cpu": "ΚΜΕ", "memory": "Μνήμη", "disk": "Δίσκος", "statsUnavailable": "Δεν είναι διαθέσιμα ζωντανά στατιστικά στοιχεία", "tmuxDetach": "Αποσύνδεση συνεδρίας tmux", - "uploadImage": "Μεταφορτώστε μια εικόνα στον κεντρικό υπολογιστή", - "uploadingImage": "Μεταφόρτωση...", - "pasteImage": "Επικόλληση εικόνας από το πρόχειρο (μόνο εικόνες)", "openFiles": "Άνοιγμα Αρχείων", "openDocker": "Άνοιγμα Docker", "openTunnel": "Ανοιχτή σήραγγα", @@ -3285,31 +3136,6 @@ "timeHours": "{{count}}πριν από ώρες", "timeDays": "{{count}}πριν από ημέρες" }, - "mobileKeyboard": { - "shift": "Αλλαγή", - "ctrl": "Ctrl", - "esc": "Esc", - "tab": "Αυτί", - "backTab": "⇥", - "arrowUp": "Βέλος προς τα πάνω", - "arrowDown": "Βέλος προς τα κάτω", - "arrowLeft": "Βέλος αριστερά", - "arrowRight": "Βέλος δεξιά", - "home": "Σπίτι", - "end": "Τέλος", - "pageUp": "PgUp", - "pageDown": "PgDn", - "delete": "Διαγραφή", - "paste": "Πάστα", - "editQuickKeys": "Επεξεργασία γρήγορων πλήκτρων", - "quickKeysTitle": "Γρήγορα πλήκτρα", - "quickKeysDesc": "Πατήστε × για κατάργηση. Υποστηρίζει έως και 8 χαρακτήρες.", - "quickKeyPlaceholder": "π.χ. sudo ", - "addQuickKey": "Προσθέτω", - "removeQuickKey": "Αφαιρώ", - "resetDefaults": "Επαναφορά στις προεπιλογές", - "done": "Γινώμενος" - }, "serial": { "title": "Σειράς", "portLabel": "Λιμάνι", diff --git a/src/ui/locales/translated/es_ES.json b/src/ui/locales/translated/es_ES.json index 2ac598bca..1d0177685 100644 --- a/src/ui/locales/translated/es_ES.json +++ b/src/ui/locales/translated/es_ES.json @@ -742,7 +742,6 @@ "addHost": "Añadir host", "editHost": "Editar host", "cloneHost": "Clonar host", - "enableTerminal": "Activar terminal", "enableTunnel": "Activar túnel", "enableFileManager": "Activar gestor de archivos", "enableDocker": "Activar Docker", @@ -1041,8 +1040,6 @@ "enableSessionLoggingDesc": "Registrar la salida de la sesión de terminal para su revisión posterior", "allowSessionSharing": "Permitir compartir la sesión", "allowSessionSharingDesc": "Permitir que las sesiones en vivo en este servidor se compartan mediante un enlace o con otros usuarios.", - "enableCommandHistory": "Historial de comandos", - "enableCommandHistoryDesc": "Registrar los comandos ejecutados en este terminal para el historial y autocompletado", "localEchoLabel": "Eco local", "localEchoDesc": "Predice localmente las pulsaciones de teclas seguras para imprimir en conexiones lentas. Las solicitudes de contraseña y la entrada de control nunca se repiten.", "localEchoDefault": "Utilizar la configuración global", @@ -1072,8 +1069,6 @@ "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.", - "enableTerminalToolbar": "Habilitar la barra de herramientas del terminal", - "enableTerminalToolbarDesc": "Mostrar una barra de herramientas de acceso rápido acoplada al terminal activo para las herramientas y estadísticas en tiempo real de este host.", "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.", @@ -1550,9 +1545,7 @@ "links": "Enlaces", "navigate": "Navegar", "select": "Seleccionar", - "toggleWith": "Alternar con", - "localTerminal": "Terminal local", - "localTerminalDesc": "Abrir una consola en este ordenador" + "toggleWith": "Alternar con" }, "splitScreen": { "paneEmpty": "Panel {{index}} - vacío", @@ -1717,57 +1710,13 @@ "loadSharesFailed": "No se pudieron cargar los recursos compartidos activos." }, "terminal": { - "sessionExpiredNotice": "Tu sesión anterior en {{host}} expiró después de {{minutes}} minutos desconectada; la consola y todo lo que se ejecutaba en ella se han perdido. Habilita Auto-Tmux en este host para mantener las sesiones activas.", - "sessionExpiredNoticeNoMinutes": "Tu sesión anterior en {{host}} caducó mientras estabas desconectado; la consola y todo lo que se ejecutaba en ella se han perdido. Habilita Auto-Tmux en este host para mantener las sesiones activas.", - "enableAutoTmuxAction": "Habilitar Auto-Tmux", - "autoTmuxEnabled": "Auto-Tmux habilitado para {{host}}. Se aplica a partir de la siguiente conexión.", - "autoTmuxEnableFailed": "No se pudo habilitar Auto-Tmux.", "connect": "Conectar al host", "clear": "Limpiar", "paste": "Pegar", - "reconnect": "Reconectar", - "connectionLost": "Conexión perdida", "connected": "Conectado", "clipboardWriteFailed": "Error al copiar al portapapeles. Asegúrese de que la página se sirva a través de HTTPS o en localhost.", "clipboardReadFailed": "No se pudo leer del portapapeles. Asegúrese de que los permisos del portapapeles estén concedidos.", - "clipboardHttpWarning": "No se pudo leer el portapapeles. Algunos navegadores restringen el acceso al portapapeles fuera de HTTPS; intente usar Ctrl+V o ejecute Termix a través de HTTPS.", - "passwordPromptFillTitle": "¿Rellenar este campo con la contraseña guardada?", - "unknownError": "Se ha producido un error desconocido.", - "websocketError": "Error de conexión WebSocket", - "connecting": "Conectando...", - "noHostSelected": "Ningún host seleccionado", - "reconnecting": "Reconectando... ({{attempt}}/{{max}})", - "reconnected": "Reconectado correctamente", - "tmuxSessionCreated": "Sesión tmux creada: {{name}}", - "tmuxSessionAttached": "Sesión tmux adjuntada: {{name}}", - "tmuxUnavailable": "tmux no está instalado en el host remoto, recurriendo al intérprete de comandos estándar", - "tmuxSessionPickerTitle": "Sesiones tmux", - "tmuxSessionPickerDesc": "Se encontraron sesiones tmux existentes en este host. Seleccione una para readjuntar o cree una nueva sesión.", - "tmuxWindows": "Ventanas", - "tmuxWindowCount": "{{count}} ventana", - "tmuxAttached": "Clientes adjuntos", - "tmuxAttachedCount": "{{count}} adjunto", - "tmuxLastActivity": "Última actividad", - "tmuxTimeJustNow": "ahora mismo", - "tmuxTimeMinutes": "hace {{count}}m", - "tmuxTimeHours": "hace {{count}}h", - "tmuxTimeDays": "hace {{count}}d", - "tmuxCreateNew": "Iniciar nueva sesión", - "tmuxCopyHint": "Ajuste la selección y pulse Intro para copiar al portapapeles", "tmuxDetach": "Desconectar de la sesión tmux", - "tmuxDetached": "Desconectado de la sesión tmux", - "searchPlaceholder": "Encontrar", - "searchCaseSensitive": "Caja de cerillas", - "searchWholeWord": "Emparejar palabra completa", - "searchRegex": "Utilizar expresiones regulares", - "searchNoResults": "No hay resultados", - "searchResultCount": "{{index}} de {{count}}", - "searchNext": "Siguiente partido (Entrar)", - "searchPrevious": "Coincidencia anterior (Shift+Enter)", - "searchClose": "Cerrar (Escape)", - "maxReconnectAttemptsReached": "Número máximo de intentos de reconexión alcanzado", - "closeTab": "Cerrar", - "connectionTimeout": "Timeout de conexión", "terminalTitle": "Terminal - {{host}}", "terminalWithPath": "Terminal - {{host}}:{{path}}", "runTitle": "Ejecutando {{command}} - {{host}}", @@ -1790,27 +1739,14 @@ "opksshOpenBrowser": "Abrir navegador para autenticarse", "opksshWaitingForAuth": "Esperando autenticación en el navegador...", "opksshAuthenticating": "Procesando autenticación...", - "opksshTimeout": "La autenticación expiró. Inténtalo de nuevo.", - "opksshAuthFailed": "La autenticación falló. Revisa tus credenciales e inténtalo de nuevo.", "opksshSignInWith": "Iniciar sesión con {{provider}}", - "vaultAuthTitle": "Inicio de sesión en Vault requerido", - "vaultAuthDescription": "Se ha abierto una ventana para iniciar sesión en HashiCorp Vault. Completa el inicio de sesión allí; esta conexión continuará automáticamente.", - "vaultAuthFailed": "La autenticación de Vault falló. Inténtalo de nuevo.", - "vaultReopen": "Reabrir ventana de inicio de sesión", "sudoPasswordPopupTitle": "¿Introducir contraseña?", - "linkDialogTitle": "Abrir enlace", - "linkDialogOpen": "Abrir", - "linkDialogCopy": "Copiar", - "websocketAbnormalClose": "La conexión se cerró de forma inesperada. Esto puede deberse a un problema con el proxy inverso o la configuración SSL. Revisa los registros del servidor.", "connectionLogTitle": "Registro de conexión", "connectionLogCopy": "Copiar registros al portapapeles", "connectionLogEmpty": "Aún no hay registros de conexión", "connectionLogWaiting": "Esperando registros de conexión...", "connectionLogCopied": "Registros de conexión copiados al portapapeles", "connectionLogCopyFailed": "Error al copiar los registros al portapapeles", - "connectionRejected": "Conexión rechazada por el servidor. Revisa tu autenticación y configuración de red.", - "hostKeyRejected": "Verificación de clave de host SSH rechazada. Conexión cancelada.", - "sessionTakenOver": "La sesión se abrió en otra pestaña. Reconectando...", "split": { "splitTab": "Dividir pestaña", "addToSplit": "Añadir a la división", @@ -2225,55 +2161,6 @@ "sectionApiKeys": "Claves API", "sectionAuditLog": "Registro de auditoría", "sectionSsl": "SSL / Let's Encrypt", - "sectionTouchInput": "Entrada táctil", - "touchEnabled": "Habilitar la entrada táctil", - "touchEnabledDesc": "Permitir el desplazamiento táctil con un solo dedo en terminales.", - "touchMomentumEnabled": "Habilitar el impulso", - "touchMomentumEnabledDesc": "Continúa desplazándote brevemente después de deslizar el dedo.", - "touchAdvanced": "Avanzado", - "touchDragThreshold": "Umbral de arrastre", - "touchMaxWheelDelta": "Delta máximo de rueda", - "touchMomentumSampleWindow": "Ventana de muestreo de Momentum", - "touchReleaseGracePeriod": "Período de gracia de liberación", - "touchMinimumVelocity": "Velocidad mínima", - "touchMaximumVelocity": "Velocidad máxima", - "touchMaximumDuration": "Duración máxima", - "touchMaximumTravel": "Viaje máximo", - "touchDecayTime": "Tiempo de decaimiento", - "touchPixelsPerTick": "Píxeles por tick", - "touchMaximumFrameInterval": "Intervalo máximo de fotogramas", - "touchMaximumTicksPerFrame": "Número máximo de ticks por fotograma", - "touchResetDefaults": "Restablecer valores predeterminados", - "touchSaved": "Se han guardado los ajustes de entrada táctil.", - "touchSaveFailed": "No se pudieron guardar los ajustes de entrada táctil.", - "sectionImageStorage": "Almacenamiento de imágenes", - "imageStorageMode": "Modo de almacenamiento", - "imageStorageModeDesc": "Donde se escriben las imágenes de terminal pegadas y cargadas. La selección automática por conexión, local y remota (SFTP) es fija.", - "imageStorageModeAuto": "Auto", - "imageStorageModeLocal": "Local", - "imageStorageModeRemoteSftp": "Remoto (SFTP)", - "imageStorageLocalDir": "Directorio local", - "imageStorageLocalDirDesc": "Ruta absoluta en el servidor de Termix. No se muestra después de guardar por motivos de seguridad.", - "imageStorageLocalDirPlaceholder": "Déjelo vacío para mantener actualizado", - "imageStorageHostPath": "Ruta del host", - "imageStorageHostPathDesc": "La vista del directorio local que reciben los agentes de terminal desde el lado del host.", - "imageStorageTtl": "TTL (ms)", - "imageStorageMaxCount": "Número máximo de imágenes", - "imageStorageMaxBytes": "Bytes de almacenamiento máximo", - "imageStorageSaved": "Configuración de almacenamiento de imágenes guardada", - "imageStorageSaveFailed": "No se pudieron guardar los ajustes de almacenamiento de imágenes.", - "imageStorageInstanceId": "ID de instancia de sesión de terminal", - "imageStorageInstanceIdDesc": "ID de instancia de una de las pestañas de terminal conectadas, utilizada para probar la visibilidad.", - "imageStorageInstanceIdRequired": "Primero, introduzca un ID de instancia de sesión de terminal.", - "imageStorageTest": "Prueba", - "imageStorageTesting": "Pruebas...", - "imageStorageTestFailed": "No se pudo probar el almacenamiento de imágenes.", - "imageStorageTestConnected": "La sesión de terminal está conectada.", - "imageStorageTestNotConnected": "No existe ninguna sesión de terminal conectada con este ID de instancia.", - "imageStorageTestSelectedMode": "Una carga usaría: {{mode}}", - "imageStorageSelectedModeUnavailable": "Indisponible", - "imageStorageTestLocalVisible": "El directorio local es visible desde el host conectado.", - "imageStorageTestLocalNotVisible": "El directorio local NO es visible desde el host conectado.", "sectionBranding": "Etiqueta blanca", "brandingAppName": "Nombre de la aplicación", "brandingAppNameDesc": "Se muestra en la pantalla de inicio de sesión y en la pestaña del navegador. Déjelo en blanco para usar \"Termix\".", @@ -2354,9 +2241,6 @@ "oidcSilentLoginDefaultLockedDesc": "Esta configuración está bloqueada por la variable de entorno OIDC_SILENT_LOGIN_DEFAULT y no se puede cambiar aquí.", "allowPasswordReset": "Permitir restablecimiento de contraseña", "allowPasswordResetDesc": "Código de restablecimiento a través de los registros de Docker", - "commandHistoryEnabled": "Historial de comandos", - "commandHistoryEnabledDesc": "Permitir el registro del historial de comandos. Si se desactiva, el historial no se guarda independientemente de la configuración por host.", - "updateCommandHistoryFailed": "Error al actualizar la configuración del historial de comandos", "analyticsEnabled": "Compartir estadísticas de uso anónimo", "analyticsEnabledDesc": "Envía un recuento diario anónimo de usuarios, hosts y uso de funciones para ayudar a mejorar Termix. No se incluyen datos personales ni detalles de conexión.", "analyticsEnabledLockedDesc": "Esta configuración está bloqueada por la variable de entorno ENABLE_TELEMETRY y no se puede cambiar aquí.", @@ -2647,11 +2531,6 @@ "hostDefaultsTerminal": "Terminal", "hostDefaultsAutoTmux": "Auto-Tmux", "hostDefaultsAutoTmuxDesc": "Ejecuta las terminales de los nuevos hosts dentro de tmux para que las sesiones sobrevivan a las desconexiones (requiere tmux en el host).", - "terminalSessionTimeout": "Persistencia de sesión terminal", - "terminalSessionTimeoutDesc": "¿Cuánto tiempo permanece activa una terminal desconectada en el servidor antes de cerrarse? Mínimo 1 minuto · Máximo 1440 minutos. Para sesiones que deban durar más tiempo, active Auto-Tmux en el host.", - "terminalSessionTimeoutRange": "Introduzca un valor entre 1 y 1440 minutos.", - "terminalSessionTimeoutSaved": "Se ha guardado la persistencia de la sesión del terminal.", - "terminalSessionTimeoutSaveFailed": "No se pudo guardar la persistencia de la sesión del terminal.", "minutes": "minutos", "hostDefaultsSessionLogging": "Registro de sesión", "hostDefaultsSessionLoggingDesc": "Grabar sesiones de terminal en hosts nuevos de forma predeterminada", @@ -2708,15 +2587,6 @@ "connectToTerminal": "Conectar a Terminal", "connectToFiles": "Conectar a Archivos" }, - "history": { - "noTerminalSelected": "Ningún terminal seleccionado", - "noTerminalSelectedHint": "Abra una pestaña de terminal SSH para ver su historial de comandos", - "searchPlaceholder": "Buscar en el historial...", - "clearAll": "Borrar todo", - "noHistoryEntries": "Sin entradas de historial", - "trackingDisabled": "El seguimiento del historial está desactivado", - "trackingDisabledHint": "Actívelo en la configuración del terminal del host." - }, "sshTools": { "keyRecordingTitle": "Grabación de teclas", "recordToTerminals": "Grabar en terminales", @@ -3178,30 +3048,11 @@ "terminalToolbar": { "title": "Barra de herramientas del terminal", "showActions": "Mostrar barra de herramientas", - "hideToolbar": "Ocultar barra de herramientas", - "showToolbar": "Expandir barra de herramientas", - "moveToolbar": "Mover barra de herramientas", - "moveToolbarHint": "Arrastra para cambiar la posición de la barra de herramientas.", - "layout": "Modo de visualización de la barra de herramientas", - "layoutIcon": "Solo icono", - "layoutLabeled": "Iconos y etiquetas", - "layoutExpanded": "Ampliado con métricas", - "image": "Acciones de imagen (solo imágenes)", - "upload": "Subir imagen", - "paste": "Pegar imagen", - "detachTmux": "Desvincular tmux", - "detachTmuxDescription": "Desvincular sesión de tmux", - "imageActionFailed": "La acción de la imagen falló.", - "retry": "Rever", - "dismissError": "Descartar error de imagen", "cpu": "UPC", "memory": "Memoria", "disk": "Disco", "statsUnavailable": "Estadísticas en directo no disponibles", "tmuxDetach": "Desvincular sesión de tmux", - "uploadImage": "Sube una imagen al servidor.", - "uploadingImage": "Subiendo...", - "pasteImage": "Pegar imagen desde el portapapeles (solo imágenes)", "openFiles": "Archivos abiertos", "openDocker": "Abrir Docker", "openTunnel": "Túnel abierto", @@ -3285,31 +3136,6 @@ "timeHours": "hace {{count}}h", "timeDays": "hace {{count}}d" }, - "mobileKeyboard": { - "shift": "Mayús", - "ctrl": "Ctrl", - "esc": "Esc", - "tab": "Tab", - "backTab": "⇥", - "arrowUp": "Flecha arriba", - "arrowDown": "Flecha abajo", - "arrowLeft": "Flecha izquierda", - "arrowRight": "Flecha derecha", - "home": "Inicio", - "end": "Fin", - "pageUp": "Av Pág", - "pageDown": "Re Pág", - "delete": "Supr", - "paste": "Pasta", - "editQuickKeys": "Editar teclas rápidas", - "quickKeysTitle": "Teclas rápidas", - "quickKeysDesc": "Toca × para eliminar. Admite hasta 8 caracteres.", - "quickKeyPlaceholder": "p. ej. sudo ", - "addQuickKey": "Añadir", - "removeQuickKey": "Eliminar", - "resetDefaults": "Restablecer", - "done": "Hecho" - }, "serial": { "title": "Serial", "portLabel": "Puerto", diff --git a/src/ui/locales/translated/fi_FI.json b/src/ui/locales/translated/fi_FI.json index 89cf82c97..1338a6e6b 100644 --- a/src/ui/locales/translated/fi_FI.json +++ b/src/ui/locales/translated/fi_FI.json @@ -742,7 +742,6 @@ "addHost": "Lisää isäntä", "editHost": "Muokkaa isäntää", "cloneHost": "Klooni-isäntä", - "enableTerminal": "Ota terminaali käyttöön", "enableTunnel": "Ota tunneli käyttöön", "enableFileManager": "Ota tiedostonhallinta käyttöön", "enableDocker": "Ota Docker käyttöön", @@ -1041,8 +1040,6 @@ "enableSessionLoggingDesc": "Tallenna pääteistunnon tuloste myöhempää tarkastelua varten", "allowSessionSharing": "Salli istunnon jakaminen", "allowSessionSharingDesc": "Salli live-istuntojen jakaminen tällä isännällä linkin kautta tai muiden käyttäjien kanssa", - "enableCommandHistory": "Komentohistoria", - "enableCommandHistoryDesc": "Tallenna tässä terminaalissa suoritettavat komennot historiaa ja automaattista täydennystä varten", "localEchoLabel": "Paikallinen kaiku", "localEchoDesc": "Ennusta turvalliset tulostettavat näppäinpainallukset paikallisesti hitailla yhteyksillä. Salasanakehotteita ja ohjaussyötettä ei koskaan toisteta.", "localEchoDefault": "Käytä yleistä asetusta", @@ -1072,8 +1069,6 @@ "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", - "enableTerminalToolbar": "Ota käyttöön päätetyökalurivi", - "enableTerminalToolbarDesc": "Näytä aktiiviseen terminaaliin kiinnitetty pikakäyttöinen työkalupalkki tämän isännän työkaluille ja reaaliaikaisille tilastoille", "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.", @@ -1550,9 +1545,7 @@ "links": "Linkit", "navigate": "Navigoida", "select": "Valitse", - "toggleWith": "Vaihda", - "localTerminal": "Paikallinen terminaali", - "localTerminalDesc": "Avaa komentotulkki tällä tietokoneella" + "toggleWith": "Vaihda" }, "splitScreen": { "paneEmpty": "Ruutu {{index}} - tyhjä", @@ -1717,57 +1710,13 @@ "loadSharesFailed": "Aktiivisten jakojen lataaminen epäonnistui" }, "terminal": { - "sessionExpiredNotice": "Edellinen istuntosi kohdassa {{host}} päättyi {{minutes}} minuutin katkoksen jälkeen; komentotulkki ja kaikki siinä käynnissä oleva on poistettu. Ota automaattinen Tmux käyttöön tällä koneella pitääksesi istunnot toiminnassa.", - "sessionExpiredNoticeNoMinutes": "Edellinen istuntosi koneella {{host}} päättyi yhteyden katkettua; komentotulkki ja kaikki siinä käynnissä oleva on poissa. Ota automaattinen Tmux käyttöön tällä koneella pitääksesi istunnot toiminnassa.", - "enableAutoTmuxAction": "Ota automaattinen Tmux käyttöön", - "autoTmuxEnabled": "Auto-Tmux käytössä kohteelle {{host}}. Se on voimassa seuraavasta yhteydestä alkaen.", - "autoTmuxEnableFailed": "Automaattisen Tmuxin käyttöönotto epäonnistui", "connect": "Yhdistä isäntään", "clear": "Tyhjennä", "paste": "Liitä", - "reconnect": "Yhdistä uudelleen", - "connectionLost": "Yhteys katkesi", "connected": "Yhdistetty", "clipboardWriteFailed": "Kopiointi leikepöydälle epäonnistui. Varmista, että sivua käytetään HTTPS:n tai paikallisen isännän kautta.", "clipboardReadFailed": "Leikepöydän lukeminen epäonnistui. Varmista, että leikepöydän käyttöoikeudet on myönnetty.", - "clipboardHttpWarning": "Leikepöydän lukeminen epäonnistui. Jotkin selaimet rajoittavat leikepöydän käyttöä HTTPS-yhteyden ulkopuolella – kokeile Ctrl+V-näppäinyhdistelmää tai käytä Termixiä HTTPS-yhteyden kautta.", - "passwordPromptFillTitle": "Täytä tallennettu salasana tähän kehotteeseen?", - "unknownError": "Tuntematon virhe tapahtui", - "websocketError": "WebSocket-yhteysvirhe", - "connecting": "Yhdistetään...", - "noHostSelected": "Ei valittua isäntää", - "reconnecting": "Yhdistetään uudelleen... ({{attempt}}/{{max}})", - "reconnected": "Yhdistäminen uudelleen onnistui", - "tmuxSessionCreated": "tmux-istunto luotu: {{name}}", - "tmuxSessionAttached": "tmux-istunto liitetty: {{name}}", - "tmuxUnavailable": "tmuxia ei ole asennettu etäkoneeseen, vaan käytetään vakiokuorta.", - "tmuxSessionPickerTitle": "tmux-istunnot", - "tmuxSessionPickerDesc": "Tältä isännältä löytyi olemassa olevia tmux-istuntoja. Valitse yksi liittääksesi sen uudelleen tai luodaksesi uuden istunnon.", - "tmuxWindows": "Ikkunat", - "tmuxWindowCount": "{{count}} ikkuna", - "tmuxAttached": "Liitetyt asiakkaat", - "tmuxAttachedCount": "{{count}} liitteenä", - "tmuxLastActivity": "Viimeisin aktiviteetti", - "tmuxTimeJustNow": "juuri nyt", - "tmuxTimeMinutes": "{{count}}minuuttia sitten", - "tmuxTimeHours": "{{count}}tuntia sitten", - "tmuxTimeDays": "{{count}}d sitten", - "tmuxCreateNew": "Aloita uusi istunto", - "tmuxCopyHint": "Säädä valintaa ja kopioi leikepöydälle painamalla Enter", "tmuxDetach": "Irrota tmux-istunnosta", - "tmuxDetached": "Irrotettu tmux-istunnosta", - "searchPlaceholder": "Löytää", - "searchCaseSensitive": "Ottelukotelo", - "searchWholeWord": "Koko sanan täsmäys", - "searchRegex": "Käytä säännöllistä lauseketta", - "searchNoResults": "Ei tuloksia", - "searchResultCount": "{{index}} / {{count}}", - "searchNext": "Seuraava osuma (Enter)", - "searchPrevious": "Edellinen osuma (Vaihto+Enter)", - "searchClose": "Sulje (Esc)", - "maxReconnectAttemptsReached": "Yhteyden muodostamisen uudelleenyritysten enimmäismäärä saavutettu", - "closeTab": "Lähellä", - "connectionTimeout": "Yhteyden aikakatkaisu", "terminalTitle": "Pääte - {{host}}", "terminalWithPath": "Pääte - {{host}}:{{path}}", "runTitle": "Juoksemassa {{command}} - {{host}}", @@ -1790,27 +1739,14 @@ "opksshOpenBrowser": "Avaa selain todennusta varten", "opksshWaitingForAuth": "Odotetaan selaimen todennusta...", "opksshAuthenticating": "Todennusta käsitellään...", - "opksshTimeout": "Todennus aikakatkaistiin. Yritä uudelleen.", - "opksshAuthFailed": "Todennus epäonnistui. Tarkista tunnistetietosi ja yritä uudelleen.", "opksshSignInWith": "Kirjaudu sisään tunnuksella {{provider}}", - "vaultAuthTitle": "Holviin kirjautuminen vaaditaan", - "vaultAuthDescription": "Ikkuna HashiCorp-holviin kirjautumista varten on avautunut. Kirjaudu sisään siinä. Yhteys jatkuu automaattisesti.", - "vaultAuthFailed": "Holvin todennus epäonnistui. Yritä uudelleen.", - "vaultReopen": "Avaa kirjautumisikkuna uudelleen", "sudoPasswordPopupTitle": "Syötä salasana?", - "linkDialogTitle": "Avaa linkki", - "linkDialogOpen": "Avata", - "linkDialogCopy": "Kopioida", - "websocketAbnormalClose": "Yhteys katkesi odottamatta. Tämä voi johtua käänteisestä välityspalvelimesta tai SSL-määritysongelmasta. Tarkista palvelimen lokit.", "connectionLogTitle": "Yhteysloki", "connectionLogCopy": "Kopioi lokit leikepöydälle", "connectionLogEmpty": "Ei vielä yhteyslokeja", "connectionLogWaiting": "Yhteyslokeja odotetaan...", "connectionLogCopied": "Yhteyslokit kopioitu leikepöydälle", "connectionLogCopyFailed": "Lokien kopiointi leikepöydälle epäonnistui", - "connectionRejected": "Palvelin hylkäsi yhteyden. Tarkista todennus ja verkkoasetukset.", - "hostKeyRejected": "SSH-isännän avaimen vahvistus hylättiin. Yhteys katkaistu.", - "sessionTakenOver": "Istunto avattiin toisella välilehdellä. Yhdistetään uudelleen...", "split": { "splitTab": "Jaettu välilehti", "addToSplit": "Lisää Splitiin", @@ -2225,55 +2161,6 @@ "sectionApiKeys": "API-avaimet", "sectionAuditLog": "Tarkastusloki", "sectionSsl": "SSL / Let's Encrypt", - "sectionTouchInput": "Kosketussyöttö", - "touchEnabled": "Ota kosketussyöttö käyttöön", - "touchEnabledDesc": "Salli yhden sormen kosketusvieritys päätteissä", - "touchMomentumEnabled": "Ota vauhti käyttöön", - "touchMomentumEnabledDesc": "Jatka vierittämistä lyhyesti pyyhkäisyn jälkeen", - "touchAdvanced": "Edistynyt", - "touchDragThreshold": "Vetokynnys", - "touchMaxWheelDelta": "Suurin pyörän delta", - "touchMomentumSampleWindow": "Momentumin näyteikkuna", - "touchReleaseGracePeriod": "Vapautuksen lisäaika", - "touchMinimumVelocity": "Miniminopeus", - "touchMaximumVelocity": "Suurin nopeus", - "touchMaximumDuration": "Enimmäiskesto", - "touchMaximumTravel": "Maksimaalinen liikerata", - "touchDecayTime": "Hajoamisaika", - "touchPixelsPerTick": "Pikseleitä rastia kohden", - "touchMaximumFrameInterval": "Suurin kehysväli", - "touchMaximumTicksPerFrame": "Maksimi tikit kehystä kohden", - "touchResetDefaults": "Palauta oletusasetukset", - "touchSaved": "Kosketussyötön asetukset tallennettu", - "touchSaveFailed": "Kosketussyötteen asetusten tallentaminen epäonnistui", - "sectionImageStorage": "Kuvien tallennus", - "imageStorageMode": "Tallennustila", - "imageStorageModeDesc": "Mihin liitetyt ja ladatut päätekuvat kirjoitetaan. Automaattiset valinnat yhteyden, paikallisen ja etäyhteyden (SFTP) mukaan on korjattu.", - "imageStorageModeAuto": "Auto", - "imageStorageModeLocal": "Paikallinen", - "imageStorageModeRemoteSftp": "Etäkäyttö (SFTP)", - "imageStorageLocalDir": "Paikallinen hakemisto", - "imageStorageLocalDirDesc": "Absoluuttinen polku Termix-taustajärjestelmässä. Ei näytetä tallennuksen jälkeen turvallisuussyistä.", - "imageStorageLocalDirPlaceholder": "Jätä tyhjäksi, jos haluat pysyä ajan tasalla", - "imageStorageHostPath": "Isäntäpolku", - "imageStorageHostPathDesc": "Pääteagenttien vastaanottaman paikallisen hakemiston isäntäpuolen näkymä.", - "imageStorageTtl": "TTL (ms)", - "imageStorageMaxCount": "Kuvien enimmäismäärä", - "imageStorageMaxBytes": "Tallennustilan enimmäismäärä tavuja", - "imageStorageSaved": "Kuvien tallennusasetukset tallennettu", - "imageStorageSaveFailed": "Kuvien tallennusasetusten tallentaminen epäonnistui", - "imageStorageInstanceId": "Pääteistunnon instanssin tunnus", - "imageStorageInstanceIdDesc": "Yhdistetyn päätevälilehden instanssi-ID, jota käytetään näkyvyyden testaamiseen.", - "imageStorageInstanceIdRequired": "Anna ensin pääteistunnon instanssin tunnus", - "imageStorageTest": "Testata", - "imageStorageTesting": "Testaus...", - "imageStorageTestFailed": "Kuvan tallennuksen testaus epäonnistui", - "imageStorageTestConnected": "Pääteistunto on yhdistetty.", - "imageStorageTestNotConnected": "Tällä instanssi-ID:llä ei ole yhdistettyä pääteistuntoa.", - "imageStorageTestSelectedMode": "Lataus käyttäisi seuraavaa: {{mode}}", - "imageStorageSelectedModeUnavailable": "Ei saatavilla", - "imageStorageTestLocalVisible": "Paikallinen hakemisto on näkyvissä yhdistetyssä isännässä.", - "imageStorageTestLocalNotVisible": "Paikallinen hakemisto EI ole näkyvissä yhdistetyssä isännässä.", "sectionBranding": "Valkoinen etiketti", "brandingAppName": "Sovelluksen nimi", "brandingAppNameDesc": "Näytetään kirjautumisnäytössä ja selainvälilehdellä. Jätä tyhjäksi käyttääksesi \"Termixiä\".", @@ -2354,9 +2241,6 @@ "oidcSilentLoginDefaultLockedDesc": "Tämä asetus on lukittu OIDC_SILENT_LOGIN_DEFAULT-ympäristömuuttujalla, eikä sitä voi muuttaa täällä.", "allowPasswordReset": "Salli salasanan palautus", "allowPasswordResetDesc": "Palauta koodi Docker-lokien kautta", - "commandHistoryEnabled": "Komentohistoria", - "commandHistoryEnabledDesc": "Salli komentohistorian tallennus. Kun tämä on poistettu käytöstä, historiaa ei tallenneta isäntäkohtaisista asetuksista riippumatta.", - "updateCommandHistoryFailed": "Komentohistoria-asetuksen päivittäminen epäonnistui", "analyticsEnabled": "Jaa anonyymit käyttötilastot", "analyticsEnabledDesc": "Lähettää nimettömän päivittäisen määrän käyttäjistä, isännöistä ja ominaisuuksien käytöstä Termixin parantamiseksi. Henkilötietoja tai yhteystietoja ei koskaan sisällytetä mukaan.", "analyticsEnabledLockedDesc": "Tämä asetus on lukittu ENABLE_TELEMETRY-ympäristömuuttujalla, eikä sitä voi muuttaa täällä.", @@ -2647,11 +2531,6 @@ "hostDefaultsTerminal": "Terminaali", "hostDefaultsAutoTmux": "Auto-Tmux", "hostDefaultsAutoTmuxDesc": "Aja uusien isäntien päätteitä tmuxin sisällä, jotta istunnot selviävät katkaisuista (tarvitsee tmuxin isännässä)", - "terminalSessionTimeout": "Pääteistunnon pysyvyys", - "terminalSessionTimeoutDesc": "Kuinka kauan irrotettua päätelaitetta pidetään toiminnassa palvelimella ennen sen sulkemista. Min 1 · Max 1440 minuuttia. Jos istunnon on kestettävä tätä pidempään, ota automaattinen Tmux käyttöön isännässä.", - "terminalSessionTimeoutRange": "Anna arvo väliltä 1–1440 minuuttia", - "terminalSessionTimeoutSaved": "Pääteistunnon pysyvyys tallennettu", - "terminalSessionTimeoutSaveFailed": "Pääteistunnon pysyvyyden tallentaminen epäonnistui", "minutes": "minuuttia", "hostDefaultsSessionLogging": "Istuntojen lokitiedot", "hostDefaultsSessionLoggingDesc": "Tallenna pääteistunnot uusille isännille oletuksena", @@ -2708,15 +2587,6 @@ "connectToTerminal": "Yhdistä terminaaliin", "connectToFiles": "Yhdistä tiedostoihin" }, - "history": { - "noTerminalSelected": "Ei valittua päätettä", - "noTerminalSelectedHint": "Avaa SSH-päätevälilehti nähdäksesi sen komentohistorian", - "searchPlaceholder": "Hakuhistoria...", - "clearAll": "Tyhjennä kaikki", - "noHistoryEntries": "Ei historiamerkintöjä", - "trackingDisabled": "Historian seuranta on poistettu käytöstä", - "trackingDisabledHint": "Ota se käyttöön isännän pääteasetuksissa." - }, "sshTools": { "keyRecordingTitle": "Näppäinten tallennus", "recordToTerminals": "Tallenna terminaaleihin", @@ -3178,30 +3048,11 @@ "terminalToolbar": { "title": "Päätetyökalurivi", "showActions": "Näytä työkalupalkki", - "hideToolbar": "Piilota työkalupalkki", - "showToolbar": "Laajenna työkalupalkkia", - "moveToolbar": "Siirrä työkalupalkkia", - "moveToolbarHint": "Vedä työkalupalkin sijaintia", - "layout": "Työkalurivin näyttötila", - "layoutIcon": "Vain kuvake", - "layoutLabeled": "Kuvakkeet ja otsikot", - "layoutExpanded": "Laajennettu mittareilla", - "image": "Kuvatoiminnot (vain kuvat)", - "upload": "Lataa kuva", - "paste": "Liitä kuva", - "detachTmux": "Irrota tmux", - "detachTmuxDescription": "Irrota tmux-istunto", - "imageActionFailed": "Kuvatoiminto epäonnistui", - "retry": "Yritä uudelleen", - "dismissError": "Hylkää kuvavirhe", "cpu": "Suoritin", "memory": "Muisti", "disk": "Levy", "statsUnavailable": "Live-tilastot eivät ole saatavilla", "tmuxDetach": "Irrota tmux-istunto", - "uploadImage": "Lataa kuva isännöintipalveluun", - "uploadingImage": "Ladataan...", - "pasteImage": "Liitä kuva leikepöydältä (vain kuvat)", "openFiles": "Avaa tiedostot", "openDocker": "Avaa Docker", "openTunnel": "Avoin tunneli", @@ -3285,31 +3136,6 @@ "timeHours": "{{count}}tuntia sitten", "timeDays": "{{count}}d sitten" }, - "mobileKeyboard": { - "shift": "Siirtää", - "ctrl": "Ctrl", - "esc": "Esc-näppäin", - "tab": "Välilehti", - "backTab": "⇥", - "arrowUp": "Nuoli ylös", - "arrowDown": "Nuoli alas", - "arrowLeft": "Nuoli vasemmalle", - "arrowRight": "Nuoli oikealle", - "home": "Kotiin", - "end": "Loppu", - "pageUp": "Sivu ylös", - "pageDown": "PgDn", - "delete": "Poista", - "paste": "Liitä", - "editQuickKeys": "Muokkaa pikanäppäimiä", - "quickKeysTitle": "Pikanäppäimet", - "quickKeysDesc": "Poista napauttamalla ×. Tukee enintään 8 merkkiä.", - "quickKeyPlaceholder": "esim. sudo ", - "addQuickKey": "Lisätä", - "removeQuickKey": "Poistaa", - "resetDefaults": "Palauta oletusasetukset", - "done": "Tehty" - }, "serial": { "title": "Sarjanumero", "portLabel": "Portti", diff --git a/src/ui/locales/translated/fr_FR.json b/src/ui/locales/translated/fr_FR.json index a2269d750..bb085eeb7 100644 --- a/src/ui/locales/translated/fr_FR.json +++ b/src/ui/locales/translated/fr_FR.json @@ -742,7 +742,6 @@ "addHost": "Ajouter un hôte", "editHost": "Modifier l'hôte", "cloneHost": "Dupliquer l'hôte", - "enableTerminal": "Activer le terminal", "enableTunnel": "Activer le tunnel", "enableFileManager": "Activer le gestionnaire de fichiers", "enableDocker": "Activer Docker", @@ -1041,8 +1040,6 @@ "enableSessionLoggingDesc": "Enregistrer la sortie de la session terminal pour une consultation ultérieure", "allowSessionSharing": "Autoriser le partage de session", "allowSessionSharingDesc": "Autoriser le partage des sessions en direct sur cet hébergeur via un lien ou avec d'autres utilisateurs", - "enableCommandHistory": "Historique des commandes", - "enableCommandHistoryDesc": "Enregistrer les commandes exécutées dans ce terminal pour l'historique et l'autocomplétion", "localEchoLabel": "Écho local", "localEchoDesc": "Prédiction locale des frappes au clavier imprimables en toute sécurité, même sur les connexions lentes. Les invites de mot de passe et les entrées de contrôle ne sont jamais affichées.", "localEchoDefault": "Utiliser les paramètres globaux", @@ -1072,8 +1069,6 @@ "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", - "enableTerminalToolbar": "Activer la barre d'outils du terminal", - "enableTerminalToolbarDesc": "Afficher une barre d'outils d'accès rapide ancrée au terminal actif pour les outils et les statistiques en direct de cet hôte.", "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.", @@ -1550,9 +1545,7 @@ "links": "Liens", "navigate": "Naviguer", "select": "Sélectionner", - "toggleWith": "Basculer avec", - "localTerminal": "Terminal local", - "localTerminalDesc": "Ouvrir un terminal sur cet ordinateur" + "toggleWith": "Basculer avec" }, "splitScreen": { "paneEmpty": "Volet {{index}} - vide", @@ -1717,57 +1710,13 @@ "loadSharesFailed": "Échec du chargement des actions actives" }, "terminal": { - "sessionExpiredNotice": "Votre session précédente sur {{host}} a expiré après {{minutes}} minutes de déconnexion ; le shell et tous les processus en cours d'exécution ont été supprimés. Activez Auto-Tmux sur cet hôte pour maintenir les sessions actives.", - "sessionExpiredNoticeNoMinutes": "Votre session précédente sur {{host}} a expiré pendant la déconnexion ; le shell et tous les processus en cours d'exécution ont été supprimés. Activez Auto-Tmux sur cet hôte pour maintenir les sessions actives.", - "enableAutoTmuxAction": "Activer Auto-Tmux", - "autoTmuxEnabled": "Auto-Tmux activé pour {{host}}. Cela s'appliquera à partir de la prochaine connexion.", - "autoTmuxEnableFailed": "Échec de l'activation d'Auto-Tmux", "connect": "Se connecter à l'hôte", "clear": "Effacer", "paste": "Coller", - "reconnect": "Reconnecter", - "connectionLost": "Connexion perdue", "connected": "Connecté", "clipboardWriteFailed": "Échec de la copie dans le presse-papiers. Assurez-vous que la page est servie en HTTPS ou localhost.", "clipboardReadFailed": "Échec de la lecture du presse-papiers. Assurez-vous que les autorisations du presse-papiers sont accordées.", - "clipboardHttpWarning": "Impossible de lire le contenu du presse-papiers. Certains navigateurs restreignent l'accès au presse-papiers en dehors du protocole HTTPS ; essayez plutôt Ctrl+V ou utilisez Termix via HTTPS.", - "passwordPromptFillTitle": "Remplir ce champ avec le mot de passe enregistré ?", - "unknownError": "Une erreur inconnue s'est produite", - "websocketError": "Erreur de connexion WebSocket", - "connecting": "Connexion en cours...", - "noHostSelected": "Aucun hôte sélectionné", - "reconnecting": "Reconnexion... ({{attempt}}/{{max}})", - "reconnected": "Reconnexion réussie", - "tmuxSessionCreated": "Session tmux créée : {{name}}", - "tmuxSessionAttached": "Session tmux attachée : {{name}}", - "tmuxUnavailable": "tmux n'est pas installé sur l'hôte distant, retour au shell standard", - "tmuxSessionPickerTitle": "Sessions tmux", - "tmuxSessionPickerDesc": "Des sessions tmux existantes ont été trouvées sur cet hôte. Sélectionnez-en une pour vous y rattacher ou créez une nouvelle session.", - "tmuxWindows": "Fenêtres", - "tmuxWindowCount": "{{count}} fenêtre(s)", - "tmuxAttached": "Clients attachés", - "tmuxAttachedCount": "{{count}} attaché(s)", - "tmuxLastActivity": "Dernière activité", - "tmuxTimeJustNow": "à l'instant", - "tmuxTimeMinutes": "il y a {{count}} min", - "tmuxTimeHours": "il y a {{count}} h", - "tmuxTimeDays": "il y a {{count}} j", - "tmuxCreateNew": "Créer une nouvelle session", - "tmuxCopyHint": "Ajustez la sélection et appuyez sur Entrée pour copier dans le presse-papiers", "tmuxDetach": "Se détacher de la session tmux", - "tmuxDetached": "Détaché de la session tmux", - "searchPlaceholder": "Trouver", - "searchCaseSensitive": "Boîte d'allumettes", - "searchWholeWord": "Associer le mot entier", - "searchRegex": "Utilisez une expression régulière", - "searchNoResults": "Aucun résultat", - "searchResultCount": "{{index}} de {{count}}", - "searchNext": "Prochain match (Entrée)", - "searchPrevious": "Match précédent (Maj+Entrée)", - "searchClose": "Fermer (Échap)", - "maxReconnectAttemptsReached": "Nombre maximal de tentatives de reconnexion atteint", - "closeTab": "Fermer", - "connectionTimeout": "Délai de connexion dépassé", "terminalTitle": "Terminal - {{host}}", "terminalWithPath": "Terminal - {{host}}:{{path}}", "runTitle": "Exécution de {{command}} - {{host}}", @@ -1790,27 +1739,14 @@ "opksshOpenBrowser": "Ouvrir le navigateur pour s'authentifier", "opksshWaitingForAuth": "En attente de l'authentification dans le navigateur...", "opksshAuthenticating": "Traitement de l'authentification...", - "opksshTimeout": "L'authentification a expiré. Veuillez réessayer.", - "opksshAuthFailed": "Échec de l'authentification. Veuillez vérifier vos identifiants et réessayer.", "opksshSignInWith": "Se connecter avec {{provider}}", - "vaultAuthTitle": "Connexion à Vault requise", - "vaultAuthDescription": "Une fenêtre s'est ouverte pour vous connecter à HashiCorp Vault. Terminez la connexion dans cette fenêtre ; cette connexion continuera automatiquement.", - "vaultAuthFailed": "Échec de l'authentification Vault. Veuillez réessayer.", - "vaultReopen": "Rouvrir la fenêtre de connexion", "sudoPasswordPopupTitle": "Insérer le mot de passe ?", - "linkDialogTitle": "Ouvrir le lien", - "linkDialogOpen": "Ouvrir", - "linkDialogCopy": "Copier", - "websocketAbnormalClose": "Connexion fermée de manière inattendue. Cela peut être dû à un problème de proxy inverse ou de configuration SSL. Veuillez vérifier les journaux du serveur.", "connectionLogTitle": "Journal de connexion", "connectionLogCopy": "Copier les journaux dans le presse-papiers", "connectionLogEmpty": "Aucun journal de connexion pour l'instant", "connectionLogWaiting": "En attente des journaux de connexion...", "connectionLogCopied": "Journaux de connexion copiés dans le presse-papiers", "connectionLogCopyFailed": "Échec de la copie des journaux dans le presse-papiers", - "connectionRejected": "Connexion rejetée par le serveur. Veuillez vérifier votre authentification et votre configuration réseau.", - "hostKeyRejected": "Vérification de la clé de l'hôte SSH rejetée. Connexion annulée.", - "sessionTakenOver": "La session a été ouverte dans un autre onglet. Reconnexion en cours...", "split": { "splitTab": "Diviser l'onglet", "addToSplit": "Ajouter à la division", @@ -2225,55 +2161,6 @@ "sectionApiKeys": "Clés API", "sectionAuditLog": "Journal d'audit", "sectionSsl": "SSL / Let's Encrypt", - "sectionTouchInput": "Entrée tactile", - "touchEnabled": "Activer l'entrée tactile", - "touchEnabledDesc": "Autoriser le défilement tactile à un doigt dans les terminaux", - "touchMomentumEnabled": "Activer l'élan", - "touchMomentumEnabledDesc": "Continuez à faire défiler brièvement après un glissement.", - "touchAdvanced": "Avancé", - "touchDragThreshold": "Seuil de glissement", - "touchMaxWheelDelta": "Delta de roue maximal", - "touchMomentumSampleWindow": "fenêtre d'échantillonnage de l'impulsion", - "touchReleaseGracePeriod": "période de grâce pour la libération", - "touchMinimumVelocity": "Vitesse minimale", - "touchMaximumVelocity": "Vitesse maximale", - "touchMaximumDuration": "Durée maximale", - "touchMaximumTravel": "Voyage maximal", - "touchDecayTime": "Temps de dégradation", - "touchPixelsPerTick": "Pixels par tick", - "touchMaximumFrameInterval": "Intervalle d'images maximal", - "touchMaximumTicksPerFrame": "Nombre maximal de ticks par image", - "touchResetDefaults": "Réinitialiser les paramètres par défaut", - "touchSaved": "Paramètres de saisie tactile enregistrés", - "touchSaveFailed": "Impossible d'enregistrer les paramètres d'entrée tactile", - "sectionImageStorage": "Stockage d'images", - "imageStorageMode": "Mode de stockage", - "imageStorageModeDesc": "Emplacement où sont écrites les images du terminal collées et téléchargées. La sélection automatique par connexion (locale et distante [SFTP]) est fixe.", - "imageStorageModeAuto": "Auto", - "imageStorageModeLocal": "Locale", - "imageStorageModeRemoteSftp": "Accès distant (SFTP)", - "imageStorageLocalDir": "Répertoire local", - "imageStorageLocalDirDesc": "Chemin absolu dans le backend Termix. Non affiché après enregistrement pour des raisons de sécurité.", - "imageStorageLocalDirPlaceholder": "Laisser vide pour maintenir le courant", - "imageStorageHostPath": "Chemin hôte", - "imageStorageHostPathDesc": "Vue côté hôte du répertoire local reçu par les agents terminaux.", - "imageStorageTtl": "TTL (ms)", - "imageStorageMaxCount": "Nombre maximal d'images", - "imageStorageMaxBytes": "Capacité de stockage maximale", - "imageStorageSaved": "Paramètres de stockage d'images enregistrés", - "imageStorageSaveFailed": "Impossible d'enregistrer les paramètres de stockage des images", - "imageStorageInstanceId": "ID d'instance de session du terminal", - "imageStorageInstanceIdDesc": "ID d'instance de l'un de vos onglets de terminal connectés, utilisé pour tester la visibilité.", - "imageStorageInstanceIdRequired": "Saisissez d'abord un ID d'instance de session de terminal", - "imageStorageTest": "Test", - "imageStorageTesting": "Essai...", - "imageStorageTestFailed": "Échec du test du stockage d'images", - "imageStorageTestConnected": "La session terminal est connectée.", - "imageStorageTestNotConnected": "Aucune session de terminal connectée avec cet ID d'instance.", - "imageStorageTestSelectedMode": "Un téléchargement utiliserait : {{mode}}", - "imageStorageSelectedModeUnavailable": "Indisponible", - "imageStorageTestLocalVisible": "Le répertoire local est visible depuis l'hôte connecté.", - "imageStorageTestLocalNotVisible": "Le répertoire local n'est PAS visible depuis l'hôte connecté.", "sectionBranding": "Marque blanche", "brandingAppName": "Nom de l'application", "brandingAppNameDesc": "Affiché sur l'écran de connexion et dans l'onglet du navigateur. Laisser vide pour utiliser « Termix ».", @@ -2354,9 +2241,6 @@ "oidcSilentLoginDefaultLockedDesc": "Ce paramètre est verrouillé par la variable d'environnement OIDC_SILENT_LOGIN_DEFAULT et ne peut pas être modifié ici.", "allowPasswordReset": "Autoriser la réinitialisation du mot de passe", "allowPasswordResetDesc": "Code de réinitialisation via les journaux Docker", - "commandHistoryEnabled": "Historique des commandes", - "commandHistoryEnabledDesc": "Autoriser l'enregistrement de l'historique des commandes. Lorsque désactivé, l'historique n'est pas sauvegardé, indépendamment des paramètres par hôte.", - "updateCommandHistoryFailed": "Échec de la mise à jour du paramètre d'historique des commandes", "analyticsEnabled": "Statistiques d'utilisation anonymes partagées", "analyticsEnabledDesc": "Envoie quotidiennement un décompte anonyme des utilisateurs, des hôtes et de l'utilisation des fonctionnalités afin d'améliorer Termix. Aucune donnée personnelle ni information de connexion n'est jamais incluse.", "analyticsEnabledLockedDesc": "Ce paramètre est verrouillé par la variable d'environnement ENABLE_TELEMETRY et ne peut pas être modifié ici.", @@ -2647,11 +2531,6 @@ "hostDefaultsTerminal": "Terminal", "hostDefaultsAutoTmux": "Auto-Tmux", "hostDefaultsAutoTmuxDesc": "Exécutez les terminaux des nouveaux hôtes à l'intérieur de tmux afin que les sessions persistent après les déconnexions (nécessite tmux sur l'hôte).", - "terminalSessionTimeout": "Persistance de session terminale", - "terminalSessionTimeoutDesc": "Durée de vie d'un terminal déconnecté sur le serveur avant sa fermeture. Minimum : 1 minute ; maximum : 1 440 minutes. Pour les sessions qui doivent durer plus longtemps, activez Auto-Tmux sur l'hôte.", - "terminalSessionTimeoutRange": "Saisissez une valeur comprise entre 1 et 1440 minutes", - "terminalSessionTimeoutSaved": "La persistance de la session du terminal a été enregistrée.", - "terminalSessionTimeoutSaveFailed": "Échec de la sauvegarde de la persistance de la session terminale", "minutes": "minutes", "hostDefaultsSessionLogging": "Journalisation des sessions", "hostDefaultsSessionLoggingDesc": "Enregistrer par défaut les sessions de terminal sur les nouveaux hôtes", @@ -2708,15 +2587,6 @@ "connectToTerminal": "Se connecter au terminal", "connectToFiles": "Se connecter aux fichiers" }, - "history": { - "noTerminalSelected": "Aucun terminal sélectionné", - "noTerminalSelectedHint": "Ouvrez un onglet de terminal SSH pour voir son historique de commandes", - "searchPlaceholder": "Rechercher dans l'historique...", - "clearAll": "Tout effacer", - "noHistoryEntries": "Aucune entrée d'historique", - "trackingDisabled": "Le suivi de l'historique est désactivé", - "trackingDisabledHint": "Activez-le dans les paramètres du terminal de l'hôte." - }, "sshTools": { "keyRecordingTitle": "Enregistrement des touches", "recordToTerminals": "Enregistrer vers les terminaux", @@ -3178,30 +3048,11 @@ "terminalToolbar": { "title": "Barre d'outils du terminal", "showActions": "Afficher la barre d'outils", - "hideToolbar": "Masquer la barre d'outils", - "showToolbar": "Barre d'outils Développer", - "moveToolbar": "Déplacer la barre d'outils", - "moveToolbarHint": "Faites glisser pour repositionner la barre d'outils", - "layout": "mode d'affichage de la barre d'outils", - "layoutIcon": "Icône uniquement", - "layoutLabeled": "Icônes et étiquettes", - "layoutExpanded": "Développé avec des indicateurs", - "image": "Actions sur les images (images uniquement)", - "upload": "Télécharger une image", - "paste": "Coller l'image", - "detachTmux": "Détacher tmux", - "detachTmuxDescription": "Détacher la session tmux", - "imageActionFailed": "L'action sur l'image a échoué", - "retry": "Réessayer", - "dismissError": "Erreur d'image", "cpu": "processeur", "memory": "Mémoire", "disk": "Disque", "statsUnavailable": "Statistiques en direct indisponibles", "tmuxDetach": "Détacher la session tmux", - "uploadImage": "Téléversez une image sur l'hôte", - "uploadingImage": "Chargement en cours...", - "pasteImage": "Coller une image depuis le presse-papiers (images uniquement)", "openFiles": "Ouvrir les fichiers", "openDocker": "Ouvrir Docker", "openTunnel": "Tunnel ouvert", @@ -3285,31 +3136,6 @@ "timeHours": "il y a {{count}} h", "timeDays": "il y a {{count}} j" }, - "mobileKeyboard": { - "shift": "Maj", - "ctrl": "Ctrl", - "esc": "Échap", - "tab": "Tab", - "backTab": "⇥", - "arrowUp": "Flèche haut", - "arrowDown": "Flèche bas", - "arrowLeft": "Flèche gauche", - "arrowRight": "Flèche droite", - "home": "Début", - "end": "Fin", - "pageUp": "PgUp", - "pageDown": "PgDn", - "delete": "Suppr", - "paste": "Coller", - "editQuickKeys": "Modifier les touches rapides", - "quickKeysTitle": "Touches rapides", - "quickKeysDesc": "Appuyez sur × pour supprimer. Prend en charge jusqu’à 8 caractères.", - "quickKeyPlaceholder": "ex. sudo ", - "addQuickKey": "Ajouter", - "removeQuickKey": "Supprimer", - "resetDefaults": "Rétablir les paramètres par défaut", - "done": "Terminé" - }, "serial": { "title": "Série", "portLabel": "Port", diff --git a/src/ui/locales/translated/he_IL.json b/src/ui/locales/translated/he_IL.json index 07281c693..f15d9094e 100644 --- a/src/ui/locales/translated/he_IL.json +++ b/src/ui/locales/translated/he_IL.json @@ -742,7 +742,6 @@ "addHost": "הוסף מארח", "editHost": "עריכת מארח", "cloneHost": "מארח משוכפל", - "enableTerminal": "הפעלת טרמינל", "enableTunnel": "הפעלת מנהרה", "enableFileManager": "הפעל את מנהל הקבצים", "enableDocker": "הפעל את Docker", @@ -1041,8 +1040,6 @@ "enableSessionLoggingDesc": "הקלט את פלט הפעלת הטרמינל לצורך סקירה מאוחרת יותר", "allowSessionSharing": "אפשר שיתוף סשנים", "allowSessionSharingDesc": "אפשר שיתוף של מפגשים חיים במארח זה באמצעות קישור או עם משתמשים אחרים", - "enableCommandHistory": "היסטוריית פקודות", - "enableCommandHistoryDesc": "פקודות הקלטה פועלות בטרמינל זה לצורך היסטוריה והשלמה אוטומטית", "localEchoLabel": "הד מקומי", "localEchoDesc": "חזה הקשות מקלדת להדפסה בטוחות באופן מקומי בחיבורים איטיים. בקשות סיסמה וקלט בקרה לעולם לא יוחזרו.", "localEchoDefault": "השתמש בהגדרה גלובלית", @@ -1072,8 +1069,6 @@ "bindHost": "מארח קשירה", "enableTmuxMonitor": "הפעל את צג Tmux", "enableTmuxMonitorDesc": "הצג מארח זה בצג Tmux והוסף את פעולות ה-tmux שלו לסרגל הצד", - "enableTerminalToolbar": "הפעלת סרגל הכלים של הטרמינל", - "enableTerminalToolbarDesc": "הצג סרגל כלים לגישה מהירה המעוגן למסוף הפעיל עבור הכלים והסטטיסטיקות החיות של מארח זה", "connectionToolbar": "סרגל הכלים לחיבור", "showConnectionToolbar": "הצג סרגל הכלים של החיבור", "showConnectionToolbarDesc": "הצג את סרגל הכלים בהפעלות SSH, RDP, VNC ו-Telnet של מחשב מארח זה. ניתן גם להסתיר אותו באופן זמני מסרגל הכלים עצמו.", @@ -1550,9 +1545,7 @@ "links": "קישורים", "navigate": "ניווט", "select": "לִבחוֹר", - "toggleWith": "החלף עם", - "localTerminal": "מסוף מקומי", - "localTerminalDesc": "פתח מעטפת במחשב הזה" + "toggleWith": "החלף עם" }, "splitScreen": { "paneEmpty": "חלונית {{index}} - ריקה", @@ -1717,57 +1710,13 @@ "loadSharesFailed": "נכשלה טעינת השיתופים הפעילים" }, "terminal": { - "sessionExpiredNotice": "הסשן הקודם שלך ב- {{host}} פג תוקפו לאחר {{minutes}} דקות של ניתוק; המעטפת וכל מה שפועל בה נעלם. הפעל את Auto-Tmux במארח זה כדי לשמור על הסשנים פעילים.", - "sessionExpiredNoticeNoMinutes": "הסשן הקודם שלך ב- {{host}} פג תוקפו בזמן ניתוק; המעטפת וכל מה שפועל בה נעלם. הפעל את Auto-Tmux במארח זה כדי לשמור על הסשנים פעילים.", - "enableAutoTmuxAction": "הפעל את Auto-Tmux", - "autoTmuxEnabled": "Auto-Tmux מופעל עבור {{host}}. זה חל מהחיבור הבא.", - "autoTmuxEnableFailed": "נכשל בהפעלת Auto-Tmux", "connect": "התחברות למארח", "clear": "בָּרוּר", "paste": "לְהַדבִּיק", - "reconnect": "התחבר מחדש", - "connectionLost": "החיבור אבד", "connected": "מְחוּבָּר", "clipboardWriteFailed": "ההעתקה ללוח נכשלה. ודא שהדף מוגש דרך HTTPS או localhost.", "clipboardReadFailed": "הקריאה מהלוח נכשלה. ודא שהרשאות הלוח ניתנות.", - "clipboardHttpWarning": "לא ניתן היה לקרוא את הלוח. חלק מהדפדפנים מגבילים את הגישה ללוח מחוץ ל-HTTPS - נסה Ctrl+V במקום זאת, או שלח את Termix דרך HTTPS.", - "passwordPromptFillTitle": "למלא את הסיסמה השמורה בשורת הפקודה הזו?", - "unknownError": "אירעה שגיאה לא ידועה", - "websocketError": "שגיאת חיבור WebSocket", - "connecting": "מְקַשֵׁר...", - "noHostSelected": "לא נבחר מארח", - "reconnecting": "מתחבר מחדש... ({{attempt}}/{{max}})", - "reconnected": "התחבר מחדש בהצלחה", - "tmuxSessionCreated": "נוצרה סשן tmux: {{name}}", - "tmuxSessionAttached": "סשן tmux מצורף: {{name}}", - "tmuxUnavailable": "tmux אינו מותקן על המארח המרוחק, חוזר למעטפת סטנדרטית", - "tmuxSessionPickerTitle": "מפגשי tmux", - "tmuxSessionPickerDesc": "נמצאו הפעלות tmux קיימות במארח זה. בחר אחת לחיבור מחדש או צור הפעלות חדשה.", - "tmuxWindows": "חלונות", - "tmuxWindowCount": "חלון {{count}}", - "tmuxAttached": "לקוחות מצורפים", - "tmuxAttachedCount": "{{count}} מצורף", - "tmuxLastActivity": "פעילות אחרונה", - "tmuxTimeJustNow": "זֶה עַתָה", - "tmuxTimeMinutes": "לפני {{count}}דקות", - "tmuxTimeHours": "{{count}}h ago", - "tmuxTimeDays": "{{count}}d ago", - "tmuxCreateNew": "התחל סשן חדש", - "tmuxCopyHint": "התאם את הבחירה ולחץ על Enter כדי להעתיק ללוח", "tmuxDetach": "ניתוק מסשן tmux", - "tmuxDetached": "מנותק מסשן tmux", - "searchPlaceholder": "לִמצוֹא", - "searchCaseSensitive": "מארז התאמה", - "searchWholeWord": "התאמת מילה שלמה", - "searchRegex": "השתמש בביטוי רגולרי", - "searchNoResults": "אין תוצאות", - "searchResultCount": "{{index}} מתוך {{count}}", - "searchNext": "המשחק הבא (Enter)", - "searchPrevious": "התאמה קודמת (Shift+Enter)", - "searchClose": "סגור (Escape)", - "maxReconnectAttemptsReached": "הגעת למספר המקסימלי של ניסיונות חיבור מחדש", - "closeTab": "לִסְגוֹר", - "connectionTimeout": "זמן קצוב לחיבור", "terminalTitle": "טרמינל - {{host}}", "terminalWithPath": "טרמינל - {{host}}:{{path}}", "runTitle": "ריצה {{command}} - {{host}}", @@ -1790,27 +1739,14 @@ "opksshOpenBrowser": "פתח את הדפדפן כדי לאמת", "opksshWaitingForAuth": "ממתין לאימות בדפדפן...", "opksshAuthenticating": "מעבד אימות...", - "opksshTimeout": "הזמן שהוקצב לאימות הסתיים. אנא נסה שוב.", - "opksshAuthFailed": "האימות נכשל. אנא בדוק את פרטי הגישה שלך ונסה שוב.", "opksshSignInWith": "התחבר באמצעות {{provider}}", - "vaultAuthTitle": "נדרשת כניסה לכספת", - "vaultAuthDescription": "נפתח חלון לכניסה לכספת HashiCorp. השלם את תהליך הכניסה שם; חיבור זה ימשיך אוטומטית.", - "vaultAuthFailed": "אימות הכספת נכשל. אנא נסה שוב.", - "vaultReopen": "פתח מחדש את חלון הכניסה", "sudoPasswordPopupTitle": "להכניס סיסמה?", - "linkDialogTitle": "פתח קישור", - "linkDialogOpen": "לִפְתוֹחַ", - "linkDialogCopy": "לְהַעְתִיק", - "websocketAbnormalClose": "החיבור נסגר באופן בלתי צפוי. ייתכן שהדבר נובע מבעיית פרוקסי הפוך או מבעיית תצורת SSL. אנא בדוק את יומני השרת.", "connectionLogTitle": "יומן חיבור", "connectionLogCopy": "העתקת יומנים ללוח", "connectionLogEmpty": "אין עדיין יומני חיבור", "connectionLogWaiting": "ממתין ליומני חיבור...", "connectionLogCopied": "יומני חיבור הועתקו ללוח", "connectionLogCopyFailed": "נכשלה העתקת יומנים ללוח", - "connectionRejected": "החיבור נדחה על ידי השרת. אנא בדוק את האימות ותצורת הרשת שלך.", - "hostKeyRejected": "אימות מפתח מארח SSH נדחה. החיבור בוטל.", - "sessionTakenOver": "הסשן נפתח בכרטיסייה אחרת. מתחבר מחדש...", "split": { "splitTab": "פיצול טאב", "addToSplit": "הוסף לפיצול", @@ -2225,55 +2161,6 @@ "sectionApiKeys": "מפתחות API", "sectionAuditLog": "יומן ביקורת", "sectionSsl": "SSL / בואו נצפין", - "sectionTouchInput": "קלט מגע", - "touchEnabled": "הפעל קלט מגע", - "touchEnabledDesc": "אפשר גלילה באמצעות מגע באצבע אחת במסופים", - "touchMomentumEnabled": "לאפשר מומנטום", - "touchMomentumEnabledDesc": "המשך גלילה קצרה לאחר החלקה", - "touchAdvanced": "מִתקַדֵם", - "touchDragThreshold": "סף גרירה", - "touchMaxWheelDelta": "דלתא גלגל מקסימלית", - "touchMomentumSampleWindow": "חלון דוגמת מומנטום", - "touchReleaseGracePeriod": "תקופת חסד לשחרור", - "touchMinimumVelocity": "מהירות מינימלית", - "touchMaximumVelocity": "מהירות מקסימלית", - "touchMaximumDuration": "משך זמן מקסימלי", - "touchMaximumTravel": "נסיעה מקסימלית", - "touchDecayTime": "זמן דעיכה", - "touchPixelsPerTick": "פיקסלים לכל טיק", - "touchMaximumFrameInterval": "מרווח פריימים מקסימלי", - "touchMaximumTicksPerFrame": "מספר מקסימלי של טיקים למסגרת", - "touchResetDefaults": "איפוס להגדרות ברירת מחדל", - "touchSaved": "הגדרות קלט מגע נשמרו", - "touchSaveFailed": "שמירת הגדרות קלט המגע נכשלה", - "sectionImageStorage": "אחסון תמונות", - "imageStorageMode": "מצב אחסון", - "imageStorageModeDesc": "היכן נכתבות תמונות טרמינל שהודבקו והועלו. בחירות אוטומטיות לפי חיבור, מקומי ומרוחק (SFTP) תוקנו.", - "imageStorageModeAuto": "אוטומטי", - "imageStorageModeLocal": "מְקוֹמִי", - "imageStorageModeRemoteSftp": "מרחוק (SFTP)", - "imageStorageLocalDir": "ספרייה מקומית", - "imageStorageLocalDirDesc": "נתיב מוחלט בקצה האחורי של Termix. לא מוצג לאחר שמירה מטעמי אבטחה.", - "imageStorageLocalDirPlaceholder": "השאר ריק כדי להישאר מעודכן", - "imageStorageHostPath": "נתיב המארח", - "imageStorageHostPathDesc": "תצוגת המארח של הספרייה המקומית שמקבלים סוכני הטרמינל.", - "imageStorageTtl": "TTL (אלפיות שנייה)", - "imageStorageMaxCount": "ספירת תמונות מקסימלית", - "imageStorageMaxBytes": "מספר בייטים מקסימלי לאחסון", - "imageStorageSaved": "הגדרות אחסון התמונה נשמרו", - "imageStorageSaveFailed": "שמירת הגדרות אחסון התמונה נכשלה", - "imageStorageInstanceId": "מזהה מופע של סשן מסוף", - "imageStorageInstanceIdDesc": "מזהה מופע של אחת מכרטיסיות הטרמינל המחוברות שלך, המשמש לבדיקת נראות.", - "imageStorageInstanceIdRequired": "הזן תחילה מזהה מופע של סשן מסוף", - "imageStorageTest": "מִבְחָן", - "imageStorageTesting": "בּוֹחֵן...", - "imageStorageTestFailed": "נכשלה בדיקת אחסון התמונה", - "imageStorageTestConnected": "סשן הטרמינל מחובר.", - "imageStorageTestNotConnected": "אין סשן מסוף מחובר עם מזהה מופע זה.", - "imageStorageTestSelectedMode": "העלאה תשתמש ב: {{mode}}", - "imageStorageSelectedModeUnavailable": "לא זמין", - "imageStorageTestLocalVisible": "הספרייה המקומית גלויה מהמארח המחובר.", - "imageStorageTestLocalNotVisible": "הספרייה המקומית אינה גלויה מהמארח המחובר.", "sectionBranding": "תווית לבנה", "brandingAppName": "שם האפליקציה", "brandingAppNameDesc": "מוצג במסך ההתחברות ובכרטיסיית הדפדפן. השאר ריק כדי להשתמש ב-\"Termix\".", @@ -2354,9 +2241,6 @@ "oidcSilentLoginDefaultLockedDesc": "הגדרה זו נעולה על ידי משתנה הסביבה OIDC_SILENT_LOGIN_DEFAULT ולא ניתן לשנות אותה כאן.", "allowPasswordReset": "אפשר איפוס סיסמה", "allowPasswordResetDesc": "איפוס קוד דרך יומני Docker", - "commandHistoryEnabled": "היסטוריית פקודות", - "commandHistoryEnabledDesc": "אפשר רישום היסטוריית פקודות. כאשר האפשרות מושבתת, ההיסטוריה לא נשמרת ללא קשר להגדרות של כל מחשב מארח.", - "updateCommandHistoryFailed": "נכשל עדכון הגדרת היסטוריית הפקודות", "analyticsEnabled": "שתף סטטיסטיקות שימוש אנונימיות", "analyticsEnabledDesc": "שולח ספירה יומית אנונימית של משתמשים, מארחים ושימוש בתכונות כדי לסייע בשיפור Termix. לא כלולים נתונים אישיים או פרטי חיבור.", "analyticsEnabledLockedDesc": "הגדרה זו נעולה על ידי משתנה הסביבה ENABLE_TELEMETRY ולא ניתן לשנות אותה כאן.", @@ -2647,11 +2531,6 @@ "hostDefaultsTerminal": "מָסוֹף", "hostDefaultsAutoTmux": "אוטו-טימוקס", "hostDefaultsAutoTmuxDesc": "הפעלת טרמינלים של מארחים חדשים בתוך tmux כך שההפעלות ישרדו ניתוקים (צריך tmux על המארח)", - "terminalSessionTimeout": "התמדה של סשן מסוף", - "terminalSessionTimeoutDesc": "כמה זמן מסוף מנותק נשאר פעיל בשרת לפני שהוא נסגר. מינימום 1 · מקסימום 1440 דקות. עבור סשנים שחייבים לשרוד יותר מ-10 דקות, הפעל את Auto-Tmux במארח.", - "terminalSessionTimeoutRange": "הזן ערך בין 1 ל-1440 דקות", - "terminalSessionTimeoutSaved": "שמירה על סשן הטרמינל נשמרה", - "terminalSessionTimeoutSaveFailed": "נכשלה שמירת שמירה על סשן הטרמינל", "minutes": "פּרוֹטוֹקוֹל", "hostDefaultsSessionLogging": "רישום מושבים", "hostDefaultsSessionLoggingDesc": "הקלטת הפעלות טרמינל במחשבים חדשים כברירת מחדל", @@ -2708,15 +2587,6 @@ "connectToTerminal": "התחברות לטרמינל", "connectToFiles": "התחברות לקבצים" }, - "history": { - "noTerminalSelected": "לא נבחר טרמינל", - "noTerminalSelectedHint": "פתח כרטיסיית מסוף SSH כדי להציג את היסטוריית הפקודות שלה", - "searchPlaceholder": "היסטוריית חיפוש...", - "clearAll": "נקה הכל", - "noHistoryEntries": "אין רשומות היסטוריה", - "trackingDisabled": "מעקב אחר היסטוריה מושבת", - "trackingDisabledHint": "הפעל זאת בהגדרות הטרמינל של המארח." - }, "sshTools": { "keyRecordingTitle": "רישום מפתח", "recordToTerminals": "הקלטה למסופים", @@ -3178,30 +3048,11 @@ "terminalToolbar": { "title": "סרגל הכלים של הטרמינל", "showActions": "הצג סרגל כלים", - "hideToolbar": "הסתר את סרגל הכלים", - "showToolbar": "הרחב את סרגל הכלים", - "moveToolbar": "הזזת סרגל הכלים", - "moveToolbarHint": "גרור כדי לשנות את מיקום סרגל הכלים", - "layout": "מצב תצוגה של סרגל הכלים", - "layoutIcon": "סמל בלבד", - "layoutLabeled": "סמלים ותוויות", - "layoutExpanded": "מורחב עם מדדים", - "image": "פעולות תמונה (תמונות בלבד)", - "upload": "העלאת תמונה", - "paste": "הדבקת תמונה", - "detachTmux": "ניתוק tmux", - "detachTmuxDescription": "ניתוק סשן tmux", - "imageActionFailed": "פעולת התמונה נכשלה", - "retry": "נסה שוב", - "dismissError": "סגור שגיאת תמונה", "cpu": "מעבד", "memory": "זֵכֶר", "disk": "דִיסק", "statsUnavailable": "סטטיסטיקות חיות לא זמינות", "tmuxDetach": "ניתוק סשן tmux", - "uploadImage": "העלה תמונה למארח", - "uploadingImage": "מעלה...", - "pasteImage": "הדבקת תמונה מהלוח (תמונות בלבד)", "openFiles": "פתיחת קבצים", "openDocker": "פתח את דוקר", "openTunnel": "מנהרה פתוחה", @@ -3285,31 +3136,6 @@ "timeHours": "{{count}}h ago", "timeDays": "{{count}}d ago" }, - "mobileKeyboard": { - "shift": "מִשׁמֶרֶת", - "ctrl": "קונטרול", - "esc": "Esc", - "tab": "טאב", - "backTab": "⇥", - "arrowUp": "חץ למעלה", - "arrowDown": "חץ למטה", - "arrowLeft": "חץ שמאלה", - "arrowRight": "חץ ימינה", - "home": "בַּיִת", - "end": "סוֹף", - "pageUp": "עמוד למעלה", - "pageDown": "PgDn", - "delete": "דל", - "paste": "לְהַדבִּיק", - "editQuickKeys": "עריכת מקשי מהירים", - "quickKeysTitle": "מקשי מהירים", - "quickKeysDesc": "הקש על × כדי להסיר. תומך בעד 8 תווים.", - "quickKeyPlaceholder": "למשל סודו ", - "addQuickKey": "לְהוֹסִיף", - "removeQuickKey": "לְהַסִיר", - "resetDefaults": "איפוס להגדרות ברירת מחדל", - "done": "נַעֲשָׂה" - }, "serial": { "title": "סִדרָתִי", "portLabel": "נָמָל", diff --git a/src/ui/locales/translated/hi_IN.json b/src/ui/locales/translated/hi_IN.json index afe8438fc..8649cb729 100644 --- a/src/ui/locales/translated/hi_IN.json +++ b/src/ui/locales/translated/hi_IN.json @@ -742,7 +742,6 @@ "addHost": "होस्ट जोड़ें", "editHost": "होस्ट संपादित करें", "cloneHost": "होस्ट क्लोन करें", - "enableTerminal": "टर्मिनल सक्षम करें", "enableTunnel": "टनल सक्षम करें", "enableFileManager": "फ़ाइल प्रबंधक सक्षम करें", "enableDocker": "Docker सक्षम करें", @@ -1041,8 +1040,6 @@ "enableSessionLoggingDesc": "बाद में समीक्षा के लिए टर्मिनल सत्र का आउटपुट रिकॉर्ड करें", "allowSessionSharing": "सेशन शेयरिंग की अनुमति दें", "allowSessionSharingDesc": "इस होस्ट पर होने वाले लाइव सेशन को लिंक के माध्यम से या अन्य उपयोगकर्ताओं के साथ साझा किया जा सकता है।", - "enableCommandHistory": "कमांड इतिहास", - "enableCommandHistoryDesc": "इतिहास और स्वतः पूर्ण के लिए इस टर्मिनल में चलाए गए कमांड रिकॉर्ड करें", "localEchoLabel": "स्थानीय प्रतिध्वनि", "localEchoDesc": "धीमे कनेक्शन पर स्थानीय रूप से सुरक्षित और प्रिंट करने योग्य कीस्ट्रोक्स का अनुमान लगाएं। पासवर्ड प्रॉम्प्ट और नियंत्रण इनपुट कभी भी प्रदर्शित नहीं होते हैं।", "localEchoDefault": "वैश्विक सेटिंग का उपयोग करें", @@ -1072,8 +1069,6 @@ "bindHost": "बाइंड होस्ट", "enableTmuxMonitor": "Tmux मॉनिटर सक्षम करें", "enableTmuxMonitorDesc": "इस होस्ट को Tmux मॉनिटर में दिखाएँ और इसकी tmux क्रियाओं को साइडबार में जोड़ें", - "enableTerminalToolbar": "टर्मिनल टूलबार को सक्षम करें", - "enableTerminalToolbarDesc": "इस होस्ट के टूल्स और लाइव स्टैट्स के लिए सक्रिय टर्मिनल से जुड़ा एक त्वरित-पहुँच टूलबार दिखाएँ।", "connectionToolbar": "कनेक्शन टूलबार", "showConnectionToolbar": "कनेक्शन टूलबार दिखाएँ", "showConnectionToolbarDesc": "इस होस्ट के SSH, RDP, VNC और Telnet सेशन में टूलबार दिखाएँ। आप इसे टूलबार से अस्थायी रूप से छिपा भी सकते हैं।", @@ -1550,9 +1545,7 @@ "links": "लिंक", "navigate": "नेविगेट करें", "select": "चुनें", - "toggleWith": "इसके साथ टॉगल करें", - "localTerminal": "स्थानीय टर्मिनल", - "localTerminalDesc": "इस कंप्यूटर पर शेल खोलें" + "toggleWith": "इसके साथ टॉगल करें" }, "splitScreen": { "paneEmpty": "पैन {{index}} - खाली", @@ -1717,57 +1710,13 @@ "loadSharesFailed": "सक्रिय शेयर लोड करने में विफल" }, "terminal": { - "sessionExpiredNotice": "{{host}} पर आपका पिछला सत्र {{minutes}} मिनट बाद समाप्त हो गया; शेल और उसमें चल रही हर चीज़ समाप्त हो गई है। सत्रों को सक्रिय रखने के लिए इस होस्ट पर ऑटो-ट्मक्स सक्षम करें।", - "sessionExpiredNoticeNoMinutes": "{{host}} पर आपका पिछला सत्र डिस्कनेक्ट होने के दौरान समाप्त हो गया; शेल और उसमें चल रही हर चीज़ नष्ट हो गई है। सत्रों को सक्रिय रखने के लिए इस होस्ट पर ऑटो-ट्मक्स सक्षम करें।", - "enableAutoTmuxAction": "ऑटो-टीमक्स सक्षम करें", - "autoTmuxEnabled": "{{host}}के लिए ऑटो-Tmux सक्षम किया गया है। यह अगले कनेक्शन से लागू होगा।", - "autoTmuxEnableFailed": "ऑटो-टीमक्स को सक्षम करने में विफल", "connect": "होस्ट से कनेक्ट करें", "clear": "साफ़ करें", "paste": "चिपकाएँ", - "reconnect": "पुनः कनेक्ट करें", - "connectionLost": "कनेक्शन खो गया", "connected": "कनेक्टेड", "clipboardWriteFailed": "क्लिपबोर्ड पर कॉपी करने में विफल। सुनिश्चित करें कि पेज HTTPS या लोकलहोस्ट पर सर्व किया गया है।", "clipboardReadFailed": "क्लिपबोर्ड से पढ़ने में विफल। सुनिश्चित करें कि क्लिपबोर्ड अनुमतियाँ दी गई हैं।", - "clipboardHttpWarning": "क्लिपबोर्ड पढ़ा नहीं जा सका। कुछ ब्राउज़र HTTPS के बाहर क्लिपबोर्ड एक्सेस को प्रतिबंधित करते हैं - इसके बजाय Ctrl+V का उपयोग करके देखें, या Termix को HTTPS पर चलाएं।", - "passwordPromptFillTitle": "क्या सहेजे गए पासवर्ड को इस प्रांप्ट में भरें?", - "unknownError": "अज्ञात त्रुटि हुई", - "websocketError": "WebSocket कनेक्शन त्रुटि", - "connecting": "कनेक्ट हो रहा है...", - "noHostSelected": "कोई होस्ट चयनित नहीं", - "reconnecting": "पुनः कनेक्ट हो रहा है... ({{attempt}}/{{max}})", - "reconnected": "पुनः कनेक्ट हुआ", - "tmuxSessionCreated": "tmux सत्र बनाया गया: {{name}}", - "tmuxSessionAttached": "tmux सत्र अटैच किया गया: {{name}}", - "tmuxUnavailable": "tmux रिमोट होस्ट पर स्थापित नहीं है, मानक शेल पर वापस लौट रहा है", - "tmuxSessionPickerTitle": "tmux सत्र", - "tmuxSessionPickerDesc": "इस होस्ट पर मौजूदा tmux सत्र पाए गए। पुनः अटैच करने के लिए एक का चयन करें या नया सत्र बनाएं।", - "tmuxWindows": "विंडो", - "tmuxWindowCount": "{{count}} विंडो", - "tmuxAttached": "अटैच किए गए क्लाइंट", - "tmuxAttachedCount": "{{count}} अटैच किए गए", - "tmuxLastActivity": "अंतिम गतिविधि", - "tmuxTimeJustNow": "अभी-अभी", - "tmuxTimeMinutes": "{{count}} मिनट पहले", - "tmuxTimeHours": "{{count}} घंटे पहले", - "tmuxTimeDays": "{{count}} दिन पहले", - "tmuxCreateNew": "नया सत्र प्रारंभ करें", - "tmuxCopyHint": "चयन समायोजित करें और क्लिपबोर्ड पर कॉपी करने के लिए Enter दबाएं", "tmuxDetach": "tmux सत्र से डिटैच करें", - "tmuxDetached": "tmux सत्र से डिटैच हो गया", - "searchPlaceholder": "खोजो", - "searchCaseSensitive": "मामले मिलाएं", - "searchWholeWord": "पूरे शब्द का मिलान करें", - "searchRegex": "नियमित अभिव्यक्ति का उपयोग करें", - "searchNoResults": "कोई परिणाम नहीं", - "searchResultCount": "{{index}} का {{count}}", - "searchNext": "अगला मैच (प्रवेश करें)", - "searchPrevious": "पिछला मैच (शिफ्ट+एंटर)", - "searchClose": "बंद करें (एस्केप)", - "maxReconnectAttemptsReached": "अधिकतम पुनः कनेक्ट प्रयास पूर्ण हो गए", - "closeTab": "बंद करें", - "connectionTimeout": "कनेक्शन टाइमआउट", "terminalTitle": "टर्मिनल - {{host}}", "terminalWithPath": "टर्मिनल - {{host}}:{{path}}", "runTitle": "{{command}} चल रहा है - {{host}}", @@ -1790,27 +1739,14 @@ "opksshOpenBrowser": "प्रमाणीकरण के लिए ब्राउज़र खोलें", "opksshWaitingForAuth": "ब्राउज़र में प्रमाणीकरण की प्रतीक्षा हो रही है...", "opksshAuthenticating": "प्रमाणीकरण प्रक्रिया में है...", - "opksshTimeout": "प्रमाणीकरण का समय समाप्त हो गया। कृपया पुनः प्रयास करें।", - "opksshAuthFailed": "प्रमाणीकरण विफल। कृपया अपने क्रेडेंशियल जाँचें और पुनः प्रयास करें।", "opksshSignInWith": "{{provider}} से साइन इन करें", - "vaultAuthTitle": "Vault साइन-इन आवश्यक है", - "vaultAuthDescription": "HashiCorp Vault में साइन इन करने के लिए एक विंडो खुल गई है। वहाँ साइन-इन पूरा करें; यह कनेक्शन स्वचालित रूप से जारी रहेगा।", - "vaultAuthFailed": "Vault प्रमाणीकरण विफल। कृपया पुनः प्रयास करें।", - "vaultReopen": "साइन-इन विंडो फिर से खोलें", "sudoPasswordPopupTitle": "पासवर्ड डालें?", - "linkDialogTitle": "लिंक खोलें", - "linkDialogOpen": "खोलें", - "linkDialogCopy": "कॉपी करें", - "websocketAbnormalClose": "कनेक्शन अप्रत्याशित रूप से बंद हो गया। यह रिवर्स प्रॉक्सी या SSL कॉन्फ़िगरेशन समस्या के कारण हो सकता है। कृपया सर्वर लॉग जाँचें।", "connectionLogTitle": "कनेक्शन लॉग", "connectionLogCopy": "क्लिपबोर्ड पर लॉग कॉपी करें", "connectionLogEmpty": "अभी तक कोई कनेक्शन लॉग नहीं", "connectionLogWaiting": "कनेक्शन लॉग की प्रतीक्षा हो रही है...", "connectionLogCopied": "कनेक्शन लॉग क्लिपबोर्ड पर कॉपी कर लिए गए हैं", "connectionLogCopyFailed": "क्लिपबोर्ड पर लॉग कॉपी करने में विफल", - "connectionRejected": "सर्वर द्वारा कनेक्शन अस्वीकृत। कृपया अपनी प्रमाणीकरण और नेटवर्क कॉन्फ़िगरेशन जाँचें।", - "hostKeyRejected": "SSH होस्ट कुंजी सत्यापन अस्वीकृत। कनेक्शन रद्द किया गया।", - "sessionTakenOver": "सत्र किसी अन्य टैब में खोला गया था। पुनः कनेक्ट हो रहा है...", "split": { "splitTab": "टैब विभाजित करें", "addToSplit": "विभाजन में जोड़ें", @@ -2225,55 +2161,6 @@ "sectionApiKeys": "API कुंजियाँ", "sectionAuditLog": "ऑडिट लॉग", "sectionSsl": "SSL / Let's Encrypt", - "sectionTouchInput": "स्पर्श निवेश", - "touchEnabled": "टच इनपुट सक्षम करें", - "touchEnabledDesc": "टर्मिनलों में एक उंगली से स्क्रॉल करने की सुविधा दें", - "touchMomentumEnabled": "गति को सक्षम करें", - "touchMomentumEnabledDesc": "स्वाइप करने के बाद थोड़ी देर स्क्रॉल करते रहें", - "touchAdvanced": "विकसित", - "touchDragThreshold": "ड्रैग थ्रेशहोल्ड", - "touchMaxWheelDelta": "अधिकतम पहिया डेल्टा", - "touchMomentumSampleWindow": "मोमेंटम सैंपल विंडो", - "touchReleaseGracePeriod": "रिहाई की मोहलत", - "touchMinimumVelocity": "न्यूनतम वेग", - "touchMaximumVelocity": "अधिकतम वेग", - "touchMaximumDuration": "अधिकतम अवधि", - "touchMaximumTravel": "अधिकतम यात्रा", - "touchDecayTime": "क्षय समय", - "touchPixelsPerTick": "पिक्सेल प्रति टिक", - "touchMaximumFrameInterval": "अधिकतम फ्रेम अंतराल", - "touchMaximumTicksPerFrame": "प्रति फ्रेम अधिकतम टिक", - "touchResetDefaults": "डिफ़ॉल्ट पर पुनः सेट करें", - "touchSaved": "टच इनपुट सेटिंग्स सहेजी गईं", - "touchSaveFailed": "टच इनपुट सेटिंग्स को सहेजने में विफल", - "sectionImageStorage": "छवि संग्रहण", - "imageStorageMode": "भंडारण मोड", - "imageStorageModeDesc": "जहां पेस्ट और अपलोड की गई टर्मिनल इमेज लिखी जाती हैं। कनेक्शन के आधार पर ऑटो पिकिंग की सुविधा उपलब्ध है, लोकल और रिमोट (SFTP) कनेक्शन दोनों ही तय हैं।", - "imageStorageModeAuto": "ऑटो", - "imageStorageModeLocal": "स्थानीय", - "imageStorageModeRemoteSftp": "रिमोट (एसएफटीपी)", - "imageStorageLocalDir": "स्थानीय निर्देशिका", - "imageStorageLocalDirDesc": "टर्मिक्स बैकएंड पर पूर्ण पथ। सुरक्षा कारणों से सहेजने के बाद नहीं दिखाया जाता है।", - "imageStorageLocalDirPlaceholder": "अपडेट रखने के लिए इसे खाली छोड़ दें।", - "imageStorageHostPath": "होस्ट पथ", - "imageStorageHostPathDesc": "टर्मिनल एजेंटों को प्राप्त होने वाली स्थानीय निर्देशिका का होस्ट-साइड दृश्य।", - "imageStorageTtl": "टीटीएल (मिलीमीटर)", - "imageStorageMaxCount": "अधिकतम छवि संख्या", - "imageStorageMaxBytes": "अधिकतम संग्रहण बाइट्स", - "imageStorageSaved": "छवि संग्रहण सेटिंग्स सहेजी गईं", - "imageStorageSaveFailed": "छवि संग्रहण सेटिंग्स को सहेजने में विफलता", - "imageStorageInstanceId": "टर्मिनल सत्र इंस्टेंस आईडी", - "imageStorageInstanceIdDesc": "आपके कनेक्टेड टर्मिनल टैब में से किसी एक का इंस्टेंस आईडी, जिसका उपयोग दृश्यता का परीक्षण करने के लिए किया जाता है।", - "imageStorageInstanceIdRequired": "सबसे पहले टर्मिनल सेशन इंस्टेंस आईडी दर्ज करें।", - "imageStorageTest": "परीक्षा", - "imageStorageTesting": "परीक्षण...", - "imageStorageTestFailed": "इमेज स्टोरेज का परीक्षण विफल रहा", - "imageStorageTestConnected": "टर्मिनल सत्र कनेक्ट हो गया है।", - "imageStorageTestNotConnected": "इस इंस्टेंस आईडी के साथ कोई कनेक्टेड टर्मिनल सेशन नहीं है।", - "imageStorageTestSelectedMode": "अपलोड करने के लिए निम्न का उपयोग किया जाएगा: {{mode}}", - "imageStorageSelectedModeUnavailable": "अनुपलब्ध", - "imageStorageTestLocalVisible": "कनेक्टेड होस्ट से लोकल डायरेक्टरी दिखाई देती है।", - "imageStorageTestLocalNotVisible": "स्थानीय डायरेक्टरी कनेक्टेड होस्ट से दिखाई नहीं देती है।", "sectionBranding": "सफेद उपनाम", "brandingAppName": "आवेदन का नाम", "brandingAppNameDesc": "लॉगिन स्क्रीन और ब्राउज़र टैब पर प्रदर्शित होता है। \"टर्मिक्स\" का उपयोग करने के लिए इसे खाली छोड़ दें।", @@ -2354,9 +2241,6 @@ "oidcSilentLoginDefaultLockedDesc": "यह सेटिंग OIDC_SILENT_LOGIN_DEFAULT पर्यावरण चर द्वारा लॉक की गई है और इसे यहां बदला नहीं जा सकता है।", "allowPasswordReset": "पासवर्ड रीसेट की अनुमति दें", "allowPasswordResetDesc": "Docker लॉग के माध्यम से रीसेट कोड", - "commandHistoryEnabled": "कमांड इतिहास", - "commandHistoryEnabledDesc": "कमांड इतिहास रिकॉर्डिंग की अनुमति दें। अक्षम होने पर, प्रति-होस्ट सेटिंग्स की परवाह किए बिना इतिहास सहेजा नहीं जाता है।", - "updateCommandHistoryFailed": "कमांड इतिहास सेटिंग अपडेट करने में विफल", "analyticsEnabled": "अनाम उपयोग के आँकड़े साझा करें", "analyticsEnabledDesc": "यह Termix को बेहतर बनाने में मदद करने के लिए उपयोगकर्ताओं, होस्टों और फ़ीचर उपयोग की दैनिक गुमनाम गणना भेजता है। इसमें कभी भी कोई व्यक्तिगत डेटा या कनेक्शन विवरण शामिल नहीं किया जाता है।", "analyticsEnabledLockedDesc": "यह सेटिंग ENABLE_TELEMETRY पर्यावरण चर द्वारा लॉक की गई है और इसे यहां बदला नहीं जा सकता है।", @@ -2647,11 +2531,6 @@ "hostDefaultsTerminal": "टर्मिनल", "hostDefaultsAutoTmux": "ऑटो-टीमक्स", "hostDefaultsAutoTmuxDesc": "नए होस्ट के टर्मिनलों को tmux के अंदर चलाएं ताकि डिस्कनेक्ट होने पर भी सेशन जारी रहें (इसके लिए होस्ट पर tmux होना आवश्यक है)।", - "terminalSessionTimeout": "टर्मिनल सत्र दृढ़ता", - "terminalSessionTimeoutDesc": "सर्वर पर डिस्कनेक्ट होने के बाद टर्मिनल को बंद करने से पहले कितनी देर तक चालू रखा जाता है। न्यूनतम 1 मिनट · अधिकतम 1440 मिनट। इससे अधिक समय तक चलने वाले सेशन के लिए, होस्ट पर ऑटो-ट्मक्स को सक्षम करें।", - "terminalSessionTimeoutRange": "1 से 1440 मिनट के बीच कोई मान दर्ज करें।", - "terminalSessionTimeoutSaved": "टर्मिनल सत्र की निरंतरता सहेजी गई", - "terminalSessionTimeoutSaveFailed": "टर्मिनल सत्र की निरंतरता को सहेजने में विफलता", "minutes": "मिनट", "hostDefaultsSessionLogging": "सत्र लॉगिंग", "hostDefaultsSessionLoggingDesc": "डिफ़ॉल्ट रूप से नए होस्ट पर टर्मिनल सत्र रिकॉर्ड करें", @@ -2708,15 +2587,6 @@ "connectToTerminal": "टर्मिनल से कनेक्ट करें", "connectToFiles": "फ़ाइलों से कनेक्ट करें" }, - "history": { - "noTerminalSelected": "कोई टर्मिनल चयनित नहीं", - "noTerminalSelectedHint": "कमांड इतिहास देखने के लिए SSH टर्मिनल टैब खोलें", - "searchPlaceholder": "इतिहास खोजें...", - "clearAll": "सभी साफ़ करें", - "noHistoryEntries": "कोई इतिहास प्रविष्टियाँ नहीं", - "trackingDisabled": "इतिहास ट्रैकिंग अक्षम है", - "trackingDisabledHint": "इसे होस्ट की टर्मिनल सेटिंग्स में सक्षम करें।" - }, "sshTools": { "keyRecordingTitle": "की रिकॉर्डिंग", "recordToTerminals": "टर्मिनलों में रिकॉर्ड करें", @@ -3178,30 +3048,11 @@ "terminalToolbar": { "title": "टर्मिनल टूलबार", "showActions": "टूलबार दिखाएँ", - "hideToolbar": "उपकरण पट्टी छिपाओ", - "showToolbar": "टूलबार का विस्तार करें", - "moveToolbar": "टूलबार को स्थानांतरित करें", - "moveToolbarHint": "टूलबार की स्थिति बदलने के लिए उसे खींचें", - "layout": "टूलबार डिस्प्ले मोड", - "layoutIcon": "केवल आइकन", - "layoutLabeled": "आइकन और लेबल", - "layoutExpanded": "मैट्रिक्स के साथ विस्तारित", - "image": "छवि क्रियाएँ (केवल छवियाँ)", - "upload": "तस्विर अपलोड करना", - "paste": "छवि चिपकाएँ", - "detachTmux": "tmux को अलग करें", - "detachTmuxDescription": "tmux सत्र को अलग करें", - "imageActionFailed": "छवि संबंधी कार्रवाई विफल रही", - "retry": "पुन: प्रयास करें", - "dismissError": "छवि त्रुटि को खारिज करें", "cpu": "CPU", "memory": "याद", "disk": "डिस्क", "statsUnavailable": "लाइव आंकड़े उपलब्ध नहीं हैं", "tmuxDetach": "tmux सत्र को अलग करें", - "uploadImage": "होस्ट पर एक छवि अपलोड करें", - "uploadingImage": "अपलोड हो रहा है...", - "pasteImage": "क्लिपबोर्ड से छवि पेस्ट करें (केवल छवियां)", "openFiles": "खुली फ़ाइलें", "openDocker": "ओपन डॉकर", "openTunnel": "खुली सुरंग", @@ -3285,31 +3136,6 @@ "timeHours": "{{count}}h पहले", "timeDays": "{{count}}d पहले" }, - "mobileKeyboard": { - "shift": "Shift", - "ctrl": "Ctrl", - "esc": "Esc", - "tab": "Tab", - "backTab": "⇥", - "arrowUp": "ऊपर तीर", - "arrowDown": "नीचे तीर", - "arrowLeft": "बाएँ तीर", - "arrowRight": "दाएँ तीर", - "home": "होम", - "end": "एंड", - "pageUp": "PgUp", - "pageDown": "PgDn", - "delete": "Del", - "paste": "पेस्ट करें", - "editQuickKeys": "त्वरित कुंजियाँ संपादित करें", - "quickKeysTitle": "त्वरित कुंजियाँ", - "quickKeysDesc": "हटाने के लिए × पर टैप करें। अधिकतम 8 वर्ण समर्थित।", - "quickKeyPlaceholder": "उदा. sudo ", - "addQuickKey": "जोड़ें", - "removeQuickKey": "हटाएँ", - "resetDefaults": "डिफ़ॉल्ट पर रीसेट करें", - "done": "पूर्ण" - }, "serial": { "title": "सीरियल", "portLabel": "पोर्ट", diff --git a/src/ui/locales/translated/hu_HU.json b/src/ui/locales/translated/hu_HU.json index 3e02f4ccc..18c19a001 100644 --- a/src/ui/locales/translated/hu_HU.json +++ b/src/ui/locales/translated/hu_HU.json @@ -742,7 +742,6 @@ "addHost": "Gazdagép hozzáadása", "editHost": "Gazdagép szerkesztése", "cloneHost": "Klónozó gazdagép", - "enableTerminal": "Terminál engedélyezése", "enableTunnel": "Alagút engedélyezése", "enableFileManager": "Fájlkezelő engedélyezése", "enableDocker": "Docker engedélyezése", @@ -1041,8 +1040,6 @@ "enableSessionLoggingDesc": "Terminál munkamenet kimenetének rögzítése későbbi áttekintés céljából", "allowSessionSharing": "Munkamenet-megosztás engedélyezése", "allowSessionSharingDesc": "Engedélyezi az élő munkamenetek megosztását ezen a gazdagépen linken vagy más felhasználókkal", - "enableCommandHistory": "Parancsnoki előzmények", - "enableCommandHistoryDesc": "A terminálon futó parancsok rögzítése előzmények és automatikus kiegészítés céljából", "localEchoLabel": "Helyi visszhang", "localEchoDesc": "Lassú kapcsolatok esetén is helyben előrejelzi a biztonságosan nyomtatható billentyűleütéseket. A jelszókérések és a vezérlőbevitel soha nem jelenik meg.", "localEchoDefault": "Globális beállítás használata", @@ -1072,8 +1069,6 @@ "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", - "enableTerminalToolbar": "Terminál eszköztár engedélyezése", - "enableTerminalToolbarDesc": "Gyorselérési eszköztár megjelenítése az aktív terminálhoz rögzítve a gazdagép eszközeihez és élő statisztikáihoz", "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.", @@ -1550,9 +1545,7 @@ "links": "Linkek", "navigate": "Navigálás", "select": "Válasszon", - "toggleWith": "Váltás erre:", - "localTerminal": "Helyi terminál", - "localTerminalDesc": "Nyisson meg egy parancsértelmezőt ezen a számítógépen" + "toggleWith": "Váltás erre:" }, "splitScreen": { "paneEmpty": "{{index}} ablaktábla - üres", @@ -1717,57 +1710,13 @@ "loadSharesFailed": "Nem sikerült betölteni az aktív megosztásokat" }, "terminal": { - "sessionExpiredNotice": "Az előző munkameneted a következő helyen: {{host}} lejárt {{minutes}} perc lecsatlakozás után; a shell és minden, ami benne fut, eltűnt. Kapcsold be az Auto-Tmux-ot ezen a gépen a munkamenetek életben tartásához.", - "sessionExpiredNoticeNoMinutes": "Az előző munkameneted a {{host}} gépen lejárt, miközben le voltál választva; a shell és minden benne futó dolog elveszett. Kapcsold be az Auto-Tmuxot ezen a gépen a munkamenetek életben tartásához.", - "enableAutoTmuxAction": "Auto-Tmux engedélyezése", - "autoTmuxEnabled": "Auto-Tmux engedélyezve a {{host}}számára. A következő kapcsolattól érvényes.", - "autoTmuxEnableFailed": "Nem sikerült engedélyezni az Auto-Tmux-ot", "connect": "Csatlakozás a gazdagéphez", "clear": "Világos", "paste": "Paszta", - "reconnect": "Újracsatlakozás", - "connectionLost": "Kapcsolat megszakadt", "connected": "Csatlakoztatva", "clipboardWriteFailed": "Nem sikerült a vágólapra másolni. Győződjön meg arról, hogy az oldal HTTPS vagy localhost protokollon keresztül jelenik meg.", "clipboardReadFailed": "Nem sikerült beolvasni a vágólapról. Győződjön meg arról, hogy a vágólapra vonatkozó engedélyek meg vannak adva.", - "clipboardHttpWarning": "Nem sikerült beolvasni a vágólapot. Néhány böngésző korlátozza a vágólap elérését HTTPS-en kívül - próbálja ki a Ctrl+V billentyűkombinációt, vagy HTTPS-en keresztül szolgáltassa ki a Termixet.", - "passwordPromptFillTitle": "Írja be a mentett jelszót ebbe a mezőbe?", - "unknownError": "Ismeretlen hiba történt", - "websocketError": "WebSocket csatlakozási hiba", - "connecting": "Kapcsolódás...", - "noHostSelected": "Nincs kiválasztva gazdagép", - "reconnecting": "Újracsatlakozás... ({{attempt}}/{{max}})", - "reconnected": "Sikeresen újrakapcsolódott", - "tmuxSessionCreated": "tmux munkamenet létrehozva: {{name}}", - "tmuxSessionAttached": "tmux munkamenet csatolva: {{name}}", - "tmuxUnavailable": "A tmux nincs telepítve a távoli gépre, a standard shell-t használja.", - "tmuxSessionPickerTitle": "tmux munkamenetek", - "tmuxSessionPickerDesc": "Meglévő tmux munkamenetek találhatók ezen a gazdagépen. Válasszon ki egyet az újracsatlakozáshoz, vagy hozzon létre egy új munkamenetet.", - "tmuxWindows": "Ablakok", - "tmuxWindowCount": "{{count}} ablak", - "tmuxAttached": "Csatlakoztatott ügyfelek", - "tmuxAttachedCount": "{{count}} csatolva", - "tmuxLastActivity": "Utolsó tevékenység", - "tmuxTimeJustNow": "éppen most", - "tmuxTimeMinutes": "{{count}}perccel ezelőtt", - "tmuxTimeHours": "{{count}}órával ezelőtt", - "tmuxTimeDays": "{{count}}nappal ezelőtt", - "tmuxCreateNew": "Új munkamenet indítása", - "tmuxCopyHint": "Módosítsa a kijelölést, és nyomja meg az Enter billentyűt a vágólapra másoláshoz", "tmuxDetach": "Leválasztás a tmux munkamenetről", - "tmuxDetached": "Leválasztva a tmux munkamenetről", - "searchPlaceholder": "Lelet", - "searchCaseSensitive": "Gyufa tok", - "searchWholeWord": "Egész szó egyezése", - "searchRegex": "Reguláris kifejezés használata", - "searchNoResults": "Nincs találat", - "searchResultCount": "{{index}} a {{count}}-ból", - "searchNext": "Következő találat (Enter)", - "searchPrevious": "Előző találat (Shift+Enter)", - "searchClose": "Bezárás (Escape)", - "maxReconnectAttemptsReached": "Elérte az újracsatlakozási kísérletek maximális számát", - "closeTab": "Közeli", - "connectionTimeout": "Kapcsolati időtúllépés", "terminalTitle": "Terminál - {{host}}", "terminalWithPath": "Terminál - {{host}}:{{path}}", "runTitle": "Futás {{command}} - {{host}}", @@ -1790,27 +1739,14 @@ "opksshOpenBrowser": "Böngésző megnyitása a hitelesítéshez", "opksshWaitingForAuth": "Várakozás a böngésző hitelesítésére...", "opksshAuthenticating": "Hitelesítés feldolgozása...", - "opksshTimeout": "A hitelesítés időtúllépést okozott. Kérjük, próbálja újra.", - "opksshAuthFailed": "Sikertelen hitelesítés. Kérjük, ellenőrizze a hitelesítő adatait, és próbálja újra.", "opksshSignInWith": "Bejelentkezés {{provider}} felhasználónévvel", - "vaultAuthTitle": "Bejelentkezés szükséges a széfbe", - "vaultAuthDescription": "Megnyílt egy ablak a HashiCorp Vaultba való bejelentkezéshez. Fejezze be a bejelentkezést ott; a kapcsolat automatikusan folytatódik.", - "vaultAuthFailed": "A trezor hitelesítése sikertelen. Próbálja újra.", - "vaultReopen": "Bejelentkezési ablak újranyitása", "sudoPasswordPopupTitle": "Jelszó beszúrása?", - "linkDialogTitle": "Link megnyitása", - "linkDialogOpen": "Nyitott", - "linkDialogCopy": "Másolat", - "websocketAbnormalClose": "A kapcsolat váratlanul megszakadt. Ennek oka lehet fordított proxy vagy SSL konfigurációs probléma. Kérjük, ellenőrizze a szervernaplókat.", "connectionLogTitle": "Kapcsolati napló", "connectionLogCopy": "Naplók másolása a vágólapra", "connectionLogEmpty": "Még nincsenek kapcsolati naplók", "connectionLogWaiting": "Várakozás a kapcsolati naplókra...", "connectionLogCopied": "Kapcsolati naplók másolva a vágólapra", "connectionLogCopyFailed": "Nem sikerült a naplókat a vágólapra másolni", - "connectionRejected": "A szerver elutasította a kapcsolatot. Kérjük, ellenőrizze a hitelesítést és a hálózati konfigurációt.", - "hostKeyRejected": "SSH host kulcs ellenőrzése elutasítva. Kapcsolat megszakítva.", - "sessionTakenOver": "A munkamenet egy másik lapon nyílt meg. Újrakapcsolódás...", "split": { "splitTab": "Osztott lap", "addToSplit": "Hozzáadás Splithez", @@ -2225,55 +2161,6 @@ "sectionApiKeys": "API-kulcsok", "sectionAuditLog": "Auditnapló", "sectionSsl": "SSL / Titkosítsuk", - "sectionTouchInput": "Érintéses bevitel", - "touchEnabled": "Érintéses bevitel engedélyezése", - "touchEnabledDesc": "Egyujjas görgetés engedélyezése a terminálokban", - "touchMomentumEnabled": "Lendület engedélyezése", - "touchMomentumEnabledDesc": "Görgessen tovább röviden egy húzás után", - "touchAdvanced": "Fejlett", - "touchDragThreshold": "Húzási küszöb", - "touchMaxWheelDelta": "Maximális kerékdelta", - "touchMomentumSampleWindow": "Lendület mintavételi ablak", - "touchReleaseGracePeriod": "Kiadási türelmi időszak", - "touchMinimumVelocity": "Minimális sebesség", - "touchMaximumVelocity": "Maximális sebesség", - "touchMaximumDuration": "Maximális időtartam", - "touchMaximumTravel": "Maximális mozgástartomány", - "touchDecayTime": "Bomlási idő", - "touchPixelsPerTick": "Pixelek száma kullancsonként", - "touchMaximumFrameInterval": "Maximális képkocka-intervallum", - "touchMaximumTicksPerFrame": "Maximális tickek képkockánként", - "touchResetDefaults": "Alapértelmezett beállítások visszaállítása", - "touchSaved": "Érintéses beviteli beállítások mentve", - "touchSaveFailed": "Nem sikerült menteni az érintéses bevitel beállításait", - "sectionImageStorage": "Képtárolás", - "imageStorageMode": "Tárolási mód", - "imageStorageModeDesc": "A beillesztett és feltöltött terminálképek írási helye. Kijavítottuk az automatikus kiválasztást kapcsolat, helyi és távoli (SFTP) alapján.", - "imageStorageModeAuto": "Autó", - "imageStorageModeLocal": "Helyi", - "imageStorageModeRemoteSftp": "Távoli (SFTP)", - "imageStorageLocalDir": "Helyi címtár", - "imageStorageLocalDirDesc": "Abszolút elérési út a Termix backend rendszeren. Biztonsági okokból mentés után nem jelenik meg.", - "imageStorageLocalDirPlaceholder": "Hagyja üresen, hogy naprakész maradjon", - "imageStorageHostPath": "Gazdagép elérési útja", - "imageStorageHostPathDesc": "A terminálügynökök által fogadott helyi könyvtár hosztoldali nézete.", - "imageStorageTtl": "TTL (ms)", - "imageStorageMaxCount": "Max. képszám", - "imageStorageMaxBytes": "Max. tárhely byte-okban", - "imageStorageSaved": "Képtárolási beállítások mentve", - "imageStorageSaveFailed": "Nem sikerült menteni a képtárolási beállításokat", - "imageStorageInstanceId": "Terminál munkamenet példányazonosítója", - "imageStorageInstanceIdDesc": "Az egyik csatlakoztatott terminálfül példányazonosítója, amelyet a láthatóság teszteléséhez használunk.", - "imageStorageInstanceIdRequired": "Először adjon meg egy terminál munkamenet-példányazonosítót", - "imageStorageTest": "Teszt", - "imageStorageTesting": "Tesztelés...", - "imageStorageTestFailed": "Nem sikerült tesztelni a képtárolást", - "imageStorageTestConnected": "A terminál munkamenet csatlakozva van.", - "imageStorageTestNotConnected": "Nincs csatlakoztatott terminál munkamenet ezzel a példányazonosítóval.", - "imageStorageTestSelectedMode": "Egy feltöltés ezt használná: {{mode}}", - "imageStorageSelectedModeUnavailable": "Nem elérhető", - "imageStorageTestLocalVisible": "A helyi könyvtár látható a csatlakoztatott gépről.", - "imageStorageTestLocalNotVisible": "A helyi könyvtár NEM látható a csatlakoztatott gépről.", "sectionBranding": "Fehér címke", "brandingAppName": "Alkalmazás neve", "brandingAppNameDesc": "Megjelenik a bejelentkezési képernyőn és a böngésző lapján. Hagyja üresen a \"Termix\" használatához.", @@ -2354,9 +2241,6 @@ "oidcSilentLoginDefaultLockedDesc": "Ezt a beállítást az OIDC_SILENT_LOGIN_DEFAULT környezeti változó zárolja, és itt nem módosítható.", "allowPasswordReset": "Jelszó-visszaállítás engedélyezése", "allowPasswordResetDesc": "Kód visszaállítása Docker naplókon keresztül", - "commandHistoryEnabled": "Parancsnoki előzmények", - "commandHistoryEnabledDesc": "Parancselőzmények rögzítésének engedélyezése. Ha le van tiltva, az előzmények nem kerülnek mentésre, függetlenül a gazdagép beállításaitól.", - "updateCommandHistoryFailed": "Nem sikerült frissíteni a parancselőzmények beállítását", "analyticsEnabled": "Névtelen használati statisztikák megosztása", "analyticsEnabledDesc": "Naponta névtelenül küldi a felhasználók, a hosztok és a funkciók használatának számát a Termix fejlesztése érdekében. Személyes adatokat vagy kapcsolati adatokat nem tartalmaz.", "analyticsEnabledLockedDesc": "Ezt a beállítást az ENABLE_TELEMETRY környezeti változó zárolja, és itt nem módosítható.", @@ -2647,11 +2531,6 @@ "hostDefaultsTerminal": "Terminál", "hostDefaultsAutoTmux": "Auto-Tmux", "hostDefaultsAutoTmuxDesc": "Új hosztok termináljait a tmuxon belül kell futtatni, hogy a munkamenetek túléljék a kapcsolatmegszakadásokat (tmux szükséges a hoszton)", - "terminalSessionTimeout": "Terminál munkamenet-megőrző", - "terminalSessionTimeoutDesc": "Mennyi ideig marad életben egy leválasztott terminál a szerveren, mielőtt bezárásra kerül. Min. 1 · Max. 1440 perc. Azoknál a munkameneteknél, amelyeknek ezt túl kell élniük, engedélyezze az Auto-Tmux funkciót a gazdagépen.", - "terminalSessionTimeoutRange": "Adjon meg egy 1 és 1440 perc közötti értéket", - "terminalSessionTimeoutSaved": "A terminál munkamenetének megmaradása mentve", - "terminalSessionTimeoutSaveFailed": "Nem sikerült menteni a terminál munkamenetének állandóságát", "minutes": "jegyzőkönyv", "hostDefaultsSessionLogging": "Munkamenet-naplózás", "hostDefaultsSessionLoggingDesc": "Terminál munkamenetek rögzítése új hosztokon alapértelmezés szerint", @@ -2708,15 +2587,6 @@ "connectToTerminal": "Csatlakozás a terminálhoz", "connectToFiles": "Kapcsolódás fájlokhoz" }, - "history": { - "noTerminalSelected": "Nincs kiválasztva terminál", - "noTerminalSelectedHint": "Nyisson meg egy SSH terminálfület a parancselőzmények megtekintéséhez", - "searchPlaceholder": "Keresési előzmények...", - "clearAll": "Összes törlése", - "noHistoryEntries": "Nincsenek előzménybejegyzések", - "trackingDisabled": "Az előzmények követése le van tiltva", - "trackingDisabledHint": "Engedélyezd a gazdagép terminálbeállításaiban." - }, "sshTools": { "keyRecordingTitle": "Kulcsfelvétel", "recordToTerminals": "Felvétel terminálokra", @@ -3178,30 +3048,11 @@ "terminalToolbar": { "title": "Terminál eszköztár", "showActions": "Eszköztár megjelenítése", - "hideToolbar": "Eszköztár elrejtése", - "showToolbar": "Eszköztár kibontása", - "moveToolbar": "Eszköztár áthelyezése", - "moveToolbarHint": "Húzza az eszköztár áthelyezéséhez", - "layout": "Eszköztár megjelenítési módja", - "layoutIcon": "Csak ikon", - "layoutLabeled": "Ikonok és címkék", - "layoutExpanded": "Metrikákkal bővítve", - "image": "Képműveletek (csak képek)", - "upload": "Kép feltöltése", - "paste": "Kép beillesztése", - "detachTmux": "tmux leválasztása", - "detachTmuxDescription": "tmux munkamenet leválasztása", - "imageActionFailed": "Képművelet sikertelen", - "retry": "Újrapróbálkozás", - "dismissError": "Képhiba elvetése", "cpu": "CPU", "memory": "Memória", "disk": "Korong", "statsUnavailable": "Élő statisztikák nem elérhetők", "tmuxDetach": "tmux munkamenet leválasztása", - "uploadImage": "Töltsön fel egy képet a tárhelyszolgáltatóra", - "uploadingImage": "Feltöltés...", - "pasteImage": "Kép beillesztése a vágólapról (csak képek)", "openFiles": "Fájlok megnyitása", "openDocker": "Nyílt Docker", "openTunnel": "Nyitott alagút", @@ -3285,31 +3136,6 @@ "timeHours": "{{count}}órával ezelőtt", "timeDays": "{{count}}nappal ezelőtt" }, - "mobileKeyboard": { - "shift": "Váltás", - "ctrl": "Ctrl", - "esc": "Esc", - "tab": "Fül", - "backTab": "⇥", - "arrowUp": "Felfelé nyíl", - "arrowDown": "Lefelé mutató nyíl", - "arrowLeft": "Balra nyíl", - "arrowRight": "Jobbra nyíl", - "home": "Otthon", - "end": "Vége", - "pageUp": "PgUp", - "pageDown": "PgDn", - "delete": "Del", - "paste": "Paszta", - "editQuickKeys": "Gyorsbillentyűk szerkesztése", - "quickKeysTitle": "Gyorsbillentyűk", - "quickKeysDesc": "Koppintson a × jelre az eltávolításhoz. Legfeljebb 8 karaktert támogat.", - "quickKeyPlaceholder": "pl. sudo ", - "addQuickKey": "Hozzáadás", - "removeQuickKey": "Eltávolítás", - "resetDefaults": "Alapértelmezett beállítások visszaállítása", - "done": "Kész" - }, "serial": { "title": "Sorozatszám", "portLabel": "Kikötő", diff --git a/src/ui/locales/translated/id_ID.json b/src/ui/locales/translated/id_ID.json index ba6531064..f0ba260b7 100644 --- a/src/ui/locales/translated/id_ID.json +++ b/src/ui/locales/translated/id_ID.json @@ -742,7 +742,6 @@ "addHost": "Tambahkan Host", "editHost": "Edit Host", "cloneHost": "Kloning Host", - "enableTerminal": "Aktifkan Terminal", "enableTunnel": "Aktifkan Terowongan", "enableFileManager": "Aktifkan Pengelola File", "enableDocker": "Aktifkan Docker", @@ -1041,8 +1040,6 @@ "enableSessionLoggingDesc": "Rekam output sesi terminal untuk ditinjau nanti.", "allowSessionSharing": "Izinkan Berbagi Sesi", "allowSessionSharingDesc": "Izinkan sesi langsung di host ini dibagikan melalui tautan atau dengan pengguna lain.", - "enableCommandHistory": "Riwayat Perintah", - "enableCommandHistoryDesc": "Rekam perintah yang dijalankan di terminal ini untuk riwayat dan pelengkapan otomatis.", "localEchoLabel": "Gema Lokal", "localEchoDesc": "Memprediksi penekanan tombol yang aman dan dapat dicetak secara lokal pada koneksi lambat. Permintaan kata sandi dan input kontrol tidak pernah diulang.", "localEchoDefault": "Gunakan pengaturan global", @@ -1072,8 +1069,6 @@ "bindHost": "Host Terikat", "enableTmuxMonitor": "Aktifkan Monitor Tmux", "enableTmuxMonitorDesc": "Tampilkan host ini di Tmux Monitor dan tambahkan aksi tmux-nya ke sidebar.", - "enableTerminalToolbar": "Aktifkan Toolbar Terminal", - "enableTerminalToolbarDesc": "Tampilkan bilah alat akses cepat yang terpasang pada terminal aktif untuk alat dan statistik langsung host ini.", "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.", @@ -1550,9 +1545,7 @@ "links": "Tautan", "navigate": "Navigasi", "select": "Memilih", - "toggleWith": "Beralih dengan", - "localTerminal": "Terminal Lokal", - "localTerminalDesc": "Buka shell di komputer ini" + "toggleWith": "Beralih dengan" }, "splitScreen": { "paneEmpty": "Panel {{index}} - kosong", @@ -1717,57 +1710,13 @@ "loadSharesFailed": "Gagal memuat saham aktif." }, "terminal": { - "sessionExpiredNotice": "Sesi Anda sebelumnya di {{host}} telah berakhir setelah {{minutes}} menit terputus; shell dan semua yang berjalan di dalamnya telah hilang. Aktifkan Auto-Tmux pada host ini untuk menjaga agar sesi tetap aktif.", - "sessionExpiredNoticeNoMinutes": "Sesi Anda sebelumnya di {{host}} telah berakhir saat terputus; shell dan semua yang berjalan di dalamnya telah hilang. Aktifkan Auto-Tmux pada host ini untuk menjaga agar sesi tetap aktif.", - "enableAutoTmuxAction": "Aktifkan Auto-Tmux", - "autoTmuxEnabled": "Auto-Tmux diaktifkan untuk {{host}}. Ini berlaku mulai koneksi berikutnya.", - "autoTmuxEnableFailed": "Gagal mengaktifkan Auto-Tmux", "connect": "Terhubung ke Host", "clear": "Jernih", "paste": "Pasta", - "reconnect": "Terhubung kembali", - "connectionLost": "Koneksi terputus", "connected": "Terhubung", "clipboardWriteFailed": "Gagal menyalin ke papan klip. Pastikan halaman disajikan melalui HTTPS atau localhost.", "clipboardReadFailed": "Gagal membaca dari clipboard. Pastikan izin clipboard telah diberikan.", - "clipboardHttpWarning": "Tidak dapat membaca isi clipboard. Beberapa browser membatasi akses clipboard di luar HTTPS - coba tekan Ctrl+V, atau sajikan Termix melalui HTTPS.", - "passwordPromptFillTitle": "Masukkan kata sandi yang tersimpan ke dalam kolom ini?", - "unknownError": "Terjadi kesalahan yang tidak diketahui.", - "websocketError": "Kesalahan koneksi WebSocket", - "connecting": "Menghubungkan...", - "noHostSelected": "Tidak ada pembawa acara yang dipilih.", - "reconnecting": "Menghubungkan kembali... ({{attempt}}/{{max}})", - "reconnected": "Berhasil terhubung kembali", - "tmuxSessionCreated": "Sesi tmux dibuat: {{name}}", - "tmuxSessionAttached": "Sesi tmux terlampir: {{name}}", - "tmuxUnavailable": "tmux tidak terpasang pada host jarak jauh, sehingga kembali menggunakan shell standar.", - "tmuxSessionPickerTitle": "Sesi tmux", - "tmuxSessionPickerDesc": "Sesi tmux yang ada ditemukan di host ini. Pilih salah satu untuk menyambungkan kembali atau membuat sesi baru.", - "tmuxWindows": "Windows", - "tmuxWindowCount": "{{count}} jendela", - "tmuxAttached": "Klien terlampir", - "tmuxAttachedCount": "{{count}} terlampir", - "tmuxLastActivity": "Aktivitas terakhir", - "tmuxTimeJustNow": "baru saja", - "tmuxTimeMinutes": "{{count}}m lalu", - "tmuxTimeHours": "{{count}}h lalu", - "tmuxTimeDays": "{{count}}d lalu", - "tmuxCreateNew": "Mulai sesi baru", - "tmuxCopyHint": "Sesuaikan pilihan dan tekan Enter untuk menyalin ke papan klip.", "tmuxDetach": "Lepaskan diri dari sesi tmux", - "tmuxDetached": "Terputus dari sesi tmux", - "searchPlaceholder": "Menemukan", - "searchCaseSensitive": "Kotak Korek Api", - "searchWholeWord": "Cocokkan Seluruh Kata", - "searchRegex": "Gunakan Ekspresi Reguler", - "searchNoResults": "Tidak ada hasil", - "searchResultCount": "{{index}} dari {{count}}", - "searchNext": "Pertandingan Berikutnya (Masuk)", - "searchPrevious": "Pertandingan Sebelumnya (Shift+Enter)", - "searchClose": "Tutup (Keluar)", - "maxReconnectAttemptsReached": "Upaya penyambungan kembali maksimum telah tercapai.", - "closeTab": "Menutup", - "connectionTimeout": "Waktu habis koneksi", "terminalTitle": "Terminal - {{host}}", "terminalWithPath": "Terminal - {{host}}:{{path}}", "runTitle": "Menjalankan {{command}} - {{host}}", @@ -1790,27 +1739,14 @@ "opksshOpenBrowser": "Buka Browser untuk Melakukan Otentikasi", "opksshWaitingForAuth": "Menunggu otentikasi di browser...", "opksshAuthenticating": "Memproses otentikasi...", - "opksshTimeout": "Autentikasi habis waktu. Silakan coba lagi.", - "opksshAuthFailed": "Autentikasi gagal. Harap periksa kredensial Anda dan coba lagi.", "opksshSignInWith": "Masuk dengan {{provider}}", - "vaultAuthTitle": "Login ke Vault diperlukan.", - "vaultAuthDescription": "Jendela telah terbuka untuk masuk ke HashiCorp Vault. Selesaikan proses masuk di sana; koneksi ini akan berlanjut secara otomatis.", - "vaultAuthFailed": "Autentikasi Vault gagal. Silakan coba lagi.", - "vaultReopen": "Buka kembali jendela masuk", "sudoPasswordPopupTitle": "Masukkan kata sandi?", - "linkDialogTitle": "Buka Tautan", - "linkDialogOpen": "Membuka", - "linkDialogCopy": "Menyalin", - "websocketAbnormalClose": "Koneksi terputus secara tidak terduga. Ini mungkin disebabkan oleh masalah konfigurasi reverse proxy atau SSL. Silakan periksa log server.", "connectionLogTitle": "Log Koneksi", "connectionLogCopy": "Salin log ke papan klip", "connectionLogEmpty": "Belum ada log koneksi.", "connectionLogWaiting": "Menunggu log koneksi...", "connectionLogCopied": "Log koneksi disalin ke papan klip.", "connectionLogCopyFailed": "Gagal menyalin log ke papan klip.", - "connectionRejected": "Koneksi ditolak oleh server. Harap periksa otentikasi dan konfigurasi jaringan Anda.", - "hostKeyRejected": "Verifikasi kunci host SSH ditolak. Koneksi dibatalkan.", - "sessionTakenOver": "Sesi dibuka di tab lain. Sedang menyambungkan kembali...", "split": { "splitTab": "Tab Terpisah", "addToSplit": "Tambahkan ke Split", @@ -2225,55 +2161,6 @@ "sectionApiKeys": "Kunci API", "sectionAuditLog": "Catatan Audit", "sectionSsl": "SSL / Let's Encrypt", - "sectionTouchInput": "Input Sentuh", - "touchEnabled": "Aktifkan Input Sentuh", - "touchEnabledDesc": "Izinkan pengguliran sentuh satu jari di terminal.", - "touchMomentumEnabled": "Aktifkan Momentum", - "touchMomentumEnabledDesc": "Lanjutkan menggulir sebentar setelah menggeser", - "touchAdvanced": "Canggih", - "touchDragThreshold": "Ambang batas seret", - "touchMaxWheelDelta": "Delta roda maksimum", - "touchMomentumSampleWindow": "Jendela sampel momentum", - "touchReleaseGracePeriod": "Masa tenggang pelepasan", - "touchMinimumVelocity": "Kecepatan minimum", - "touchMaximumVelocity": "Kecepatan maksimum", - "touchMaximumDuration": "Durasi maksimum", - "touchMaximumTravel": "Perjalanan maksimum", - "touchDecayTime": "Waktu peluruhan", - "touchPixelsPerTick": "Piksel per detik", - "touchMaximumFrameInterval": "Interval bingkai maksimum", - "touchMaximumTicksPerFrame": "Jumlah tick maksimum per frame", - "touchResetDefaults": "Kembalikan ke pengaturan default", - "touchSaved": "Pengaturan input sentuh tersimpan.", - "touchSaveFailed": "Gagal menyimpan pengaturan input sentuh.", - "sectionImageStorage": "Penyimpanan Gambar", - "imageStorageMode": "Mode Penyimpanan", - "imageStorageModeDesc": "Di sinilah gambar terminal yang disalin dan diunggah ditulis. Pemilihan otomatis berdasarkan koneksi, Lokal dan Jarak Jauh (SFTP) telah ditetapkan.", - "imageStorageModeAuto": "Mobil", - "imageStorageModeLocal": "Lokal", - "imageStorageModeRemoteSftp": "Jarak Jauh (SFTP)", - "imageStorageLocalDir": "Direktori lokal", - "imageStorageLocalDirDesc": "Jalur absolut pada backend Termix. Tidak ditampilkan setelah disimpan karena alasan keamanan.", - "imageStorageLocalDirPlaceholder": "Biarkan kosong agar tetap terkini.", - "imageStorageHostPath": "Jalur host", - "imageStorageHostPathDesc": "Tampilan sisi host dari direktori lokal yang diterima oleh agen terminal.", - "imageStorageTtl": "TTL (ms)", - "imageStorageMaxCount": "Jumlah gambar maksimum", - "imageStorageMaxBytes": "Byte penyimpanan maksimum", - "imageStorageSaved": "Pengaturan penyimpanan gambar tersimpan.", - "imageStorageSaveFailed": "Gagal menyimpan pengaturan penyimpanan gambar.", - "imageStorageInstanceId": "ID instance sesi terminal", - "imageStorageInstanceIdDesc": "ID instans dari salah satu tab terminal yang terhubung, digunakan untuk menguji visibilitas.", - "imageStorageInstanceIdRequired": "Masukkan ID instance sesi terminal terlebih dahulu.", - "imageStorageTest": "Tes", - "imageStorageTesting": "Pengujian...", - "imageStorageTestFailed": "Pengujian penyimpanan gambar gagal.", - "imageStorageTestConnected": "Sesi terminal terhubung.", - "imageStorageTestNotConnected": "Tidak ada sesi terminal yang terhubung dengan ID instance ini.", - "imageStorageTestSelectedMode": "Unggahan akan menggunakan: {{mode}}", - "imageStorageSelectedModeUnavailable": "Tidak tersedia", - "imageStorageTestLocalVisible": "Direktori lokal tersebut dapat dilihat dari host yang terhubung.", - "imageStorageTestLocalNotVisible": "Direktori lokal TIDAK terlihat dari host yang terhubung.", "sectionBranding": "Label Putih", "brandingAppName": "Nama Aplikasi", "brandingAppNameDesc": "Ditampilkan pada layar login dan tab browser. Biarkan kosong untuk menggunakan \"Termix\".", @@ -2354,9 +2241,6 @@ "oidcSilentLoginDefaultLockedDesc": "Pengaturan ini dikunci oleh variabel lingkungan OIDC_SILENT_LOGIN_DEFAULT dan tidak dapat diubah di sini.", "allowPasswordReset": "Izinkan Reset Kata Sandi", "allowPasswordResetDesc": "Reset kode melalui log Docker", - "commandHistoryEnabled": "Riwayat Perintah", - "commandHistoryEnabledDesc": "Izinkan perekaman riwayat perintah. Jika dinonaktifkan, riwayat tidak akan disimpan terlepas dari pengaturan per host.", - "updateCommandHistoryFailed": "Gagal memperbarui pengaturan riwayat perintah.", "analyticsEnabled": "Bagikan Statistik Penggunaan Anonim", "analyticsEnabledDesc": "Mengirimkan data harian anonim mengenai jumlah pengguna, host, dan penggunaan fitur untuk membantu meningkatkan Termix. Tidak ada data pribadi atau detail koneksi yang disertakan.", "analyticsEnabledLockedDesc": "Pengaturan ini dikunci oleh variabel lingkungan ENABLE_TELEMETRY dan tidak dapat diubah di sini.", @@ -2647,11 +2531,6 @@ "hostDefaultsTerminal": "Terminal", "hostDefaultsAutoTmux": "Auto-Tmux", "hostDefaultsAutoTmuxDesc": "Jalankan terminal host baru di dalam tmux agar sesi tetap berjalan meskipun terjadi pemutusan koneksi (membutuhkan tmux pada host).", - "terminalSessionTimeout": "Ketahanan Sesi Terminal", - "terminalSessionTimeoutDesc": "Berapa lama terminal yang terputus tetap aktif di server sebelum ditutup. Minimal 1 menit · Maksimal 1440 menit. Untuk sesi yang harus bertahan lebih lama dari ini, aktifkan Auto-Tmux pada host.", - "terminalSessionTimeoutRange": "Masukkan nilai antara 1 dan 1440 menit", - "terminalSessionTimeoutSaved": "Penyimpanan persistensi sesi terminal.", - "terminalSessionTimeoutSaveFailed": "Gagal menyimpan persistensi sesi terminal.", "minutes": "menit", "hostDefaultsSessionLogging": "Pencatatan Sesi", "hostDefaultsSessionLoggingDesc": "Secara default, rekam sesi terminal pada host baru.", @@ -2708,15 +2587,6 @@ "connectToTerminal": "Hubungkan ke Terminal", "connectToFiles": "Menghubungkan ke File" }, - "history": { - "noTerminalSelected": "Tidak ada terminal yang dipilih", - "noTerminalSelectedHint": "Buka tab terminal SSH untuk melihat riwayat perintahnya.", - "searchPlaceholder": "Riwayat pencarian...", - "clearAll": "Hapus Semua", - "noHistoryEntries": "Tidak ada entri riwayat.", - "trackingDisabled": "Pelacakan riwayat dinonaktifkan", - "trackingDisabledHint": "Aktifkan fitur ini di pengaturan terminal host." - }, "sshTools": { "keyRecordingTitle": "Rekaman Kunci", "recordToTerminals": "Rekam ke terminal", @@ -3178,30 +3048,11 @@ "terminalToolbar": { "title": "Toolbar Terminal", "showActions": "Tampilkan bilah alat", - "hideToolbar": "Sembunyikan bilah alat", - "showToolbar": "Perluas bilah alat", - "moveToolbar": "Pindahkan bilah alat", - "moveToolbarHint": "Seret untuk memposisikan ulang bilah alat", - "layout": "Mode tampilan bilah alat", - "layoutIcon": "Hanya ikon", - "layoutLabeled": "Ikon dan label", - "layoutExpanded": "Diperluas dengan metrik", - "image": "Aksi gambar (hanya gambar)", - "upload": "Unggah gambar", - "paste": "Tempel gambar", - "detachTmux": "Lepaskan tmux", - "detachTmuxDescription": "Lepaskan sesi tmux", - "imageActionFailed": "Aksi gambar gagal", - "retry": "Mencoba kembali", - "dismissError": "Abaikan kesalahan gambar", "cpu": "CPU", "memory": "Ingatan", "disk": "Disk", "statsUnavailable": "Statistik langsung tidak tersedia", "tmuxDetach": "Lepaskan sesi tmux", - "uploadImage": "Unggah gambar ke host", - "uploadingImage": "Sedang mengunggah...", - "pasteImage": "Tempel gambar dari clipboard (hanya gambar)", "openFiles": "Buka File", "openDocker": "Buka Docker", "openTunnel": "Terowongan Terbuka", @@ -3285,31 +3136,6 @@ "timeHours": "{{count}}h lalu", "timeDays": "{{count}}d lalu" }, - "mobileKeyboard": { - "shift": "Menggeser", - "ctrl": "Ctrl", - "esc": "Esc", - "tab": "Tab", - "backTab": "⇥", - "arrowUp": "Panah Atas", - "arrowDown": "Panah ke Bawah", - "arrowLeft": "Panah Kiri", - "arrowRight": "Panah Kanan", - "home": "Rumah", - "end": "Akhir", - "pageUp": "PgUp", - "pageDown": "PgDn", - "delete": "Del", - "paste": "Pasta", - "editQuickKeys": "Edit tombol pintasan", - "quickKeysTitle": "Tombol Pintas", - "quickKeysDesc": "Ketuk × untuk menghapus. Mendukung hingga 8 karakter.", - "quickKeyPlaceholder": "misalnya sudo ", - "addQuickKey": "Menambahkan", - "removeQuickKey": "Menghapus", - "resetDefaults": "Kembalikan ke pengaturan default", - "done": "Selesai" - }, "serial": { "title": "Serial", "portLabel": "Pelabuhan", diff --git a/src/ui/locales/translated/it_IT.json b/src/ui/locales/translated/it_IT.json index 3f94e25c5..510fb6047 100644 --- a/src/ui/locales/translated/it_IT.json +++ b/src/ui/locales/translated/it_IT.json @@ -742,7 +742,6 @@ "addHost": "Aggiungi host", "editHost": "Modifica host", "cloneHost": "Clona host", - "enableTerminal": "Abilita Terminale", "enableTunnel": "Abilita Tunnel", "enableFileManager": "Abilita File Manager", "enableDocker": "Abilita Docker", @@ -1041,8 +1040,6 @@ "enableSessionLoggingDesc": "Registra l'output della sessione del terminale per la revisione successiva", "allowSessionSharing": "Consenti la condivisione della sessione", "allowSessionSharingDesc": "Consenti la condivisione delle sessioni live su questo host tramite link o con altri utenti", - "enableCommandHistory": "Cronologia comandi", - "enableCommandHistoryDesc": "Registra i comandi eseguiti in questo terminale per la cronologia e il completamento automatico", "localEchoLabel": "Eco locale", "localEchoDesc": "Prevedi localmente le sequenze di tasti sicure e stampabili anche con connessioni lente. Le richieste di password e gli input di controllo non vengono mai ripetuti.", "localEchoDefault": "Utilizza le impostazioni globali", @@ -1072,8 +1069,6 @@ "bindHost": "Host di bind", "enableTmuxMonitor": "Abilita Monitor Tmux", "enableTmuxMonitorDesc": "Mostra questo host nel Monitor Tmux e aggiungi le sue azioni tmux alla barra laterale", - "enableTerminalToolbar": "Abilita la barra degli strumenti del terminale", - "enableTerminalToolbarDesc": "Mostra una barra degli strumenti ad accesso rapido ancorata al terminale attivo per gli strumenti e le statistiche in tempo reale di questo host.", "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.", @@ -1550,9 +1545,7 @@ "links": "Collegamenti", "navigate": "Naviga", "select": "Seleziona", - "toggleWith": "Attiva/disattiva con", - "localTerminal": "Terminale locale", - "localTerminalDesc": "Apri una shell su questo computer" + "toggleWith": "Attiva/disattiva con" }, "splitScreen": { "paneEmpty": "Pannello {{index}} - vuoto", @@ -1717,57 +1710,13 @@ "loadSharesFailed": "Impossibile caricare le azioni attive" }, "terminal": { - "sessionExpiredNotice": "La tua sessione precedente su {{host}} è scaduta dopo {{minutes}} minuti di disconnessione; la shell e tutto ciò che era in esecuzione al suo interno sono stati terminati. Abilita Auto-Tmux su questo host per mantenere attive le sessioni.", - "sessionExpiredNoticeNoMinutes": "La tua sessione precedente su {{host}} è scaduta mentre eri disconnesso; la shell e tutto ciò che era in esecuzione al suo interno sono stati eliminati. Abilita Auto-Tmux su questo host per mantenere attive le sessioni.", - "enableAutoTmuxAction": "Abilita Auto-Tmux", - "autoTmuxEnabled": "Auto-Tmux abilitato per {{host}}. Si applica dalla prossima connessione.", - "autoTmuxEnableFailed": "Impossibile abilitare Auto-Tmux", "connect": "Connetti all'host", "clear": "Cancella", "paste": "Incolla", - "reconnect": "Riconnetti", - "connectionLost": "Connessione persa", "connected": "Connesso", "clipboardWriteFailed": "Copia negli appunti non riuscita. Assicurati che la pagina sia servita via HTTPS o localhost.", "clipboardReadFailed": "Lettura dagli appunti non riuscita. Assicurati che i permessi degli appunti siano stati concessi.", - "clipboardHttpWarning": "Impossibile leggere gli appunti. Alcuni browser limitano l'accesso agli appunti al di fuori del protocollo HTTPS: prova a usare Ctrl+V oppure a utilizzare Termix tramite HTTPS.", - "passwordPromptFillTitle": "Inserire la password salvata in questa richiesta?", - "unknownError": "Si è verificato un errore sconosciuto", - "websocketError": "Errore di connessione WebSocket", - "connecting": "Connessione in corso...", - "noHostSelected": "Nessun host selezionato", - "reconnecting": "Riconnessione... ({{attempt}}/{{max}})", - "reconnected": "Riconnesso con successo", - "tmuxSessionCreated": "Sessione tmux creata: {{name}}", - "tmuxSessionAttached": "Sessione tmux collegata: {{name}}", - "tmuxUnavailable": "tmux non è installato sull'host remoto, si utilizza la shell standard", - "tmuxSessionPickerTitle": "Sessioni tmux", - "tmuxSessionPickerDesc": "Sono state trovate sessioni tmux esistenti su questo host. Selezionane una per ricollegarti o crea una nuova sessione.", - "tmuxWindows": "Finestre", - "tmuxWindowCount": "{{count}} finestra", - "tmuxAttached": "Client collegati", - "tmuxAttachedCount": "{{count}} collegato", - "tmuxLastActivity": "Ultima attività", - "tmuxTimeJustNow": "Adesso", - "tmuxTimeMinutes": "{{count}}m fa", - "tmuxTimeHours": "{{count}}h fa", - "tmuxTimeDays": "{{count}}d fa", - "tmuxCreateNew": "Avvia nuova sessione", - "tmuxCopyHint": "Regola la selezione e premi Invio per copiare negli appunti", "tmuxDetach": "Scollegati dalla sessione tmux", - "tmuxDetached": "Scollegato dalla sessione tmux", - "searchPlaceholder": "Trovare", - "searchCaseSensitive": "Scatola di fiammiferi", - "searchWholeWord": "Abbina la parola intera", - "searchRegex": "Utilizzare le espressioni regolari", - "searchNoResults": "Nessun risultato", - "searchResultCount": "{{index}} di {{count}}", - "searchNext": "Prossima partita (Invio)", - "searchPrevious": "Partita precedente (Maiusc+Invio)", - "searchClose": "Chiudi (Esc)", - "maxReconnectAttemptsReached": "Raggiunto il numero massimo di tentativi di riconnessione", - "closeTab": "Chiudi", - "connectionTimeout": "Timeout di connessione", "terminalTitle": "Terminale - {{host}}", "terminalWithPath": "Terminale - {{host}}:{{path}}", "runTitle": "Esecuzione {{command}} - {{host}}", @@ -1790,27 +1739,14 @@ "opksshOpenBrowser": "Apri il browser per autenticarti", "opksshWaitingForAuth": "In attesa dell'autenticazione nel browser...", "opksshAuthenticating": "Autenticazione in corso...", - "opksshTimeout": "Autenticazione scaduta. Riprova.", - "opksshAuthFailed": "Autenticazione fallita. Controlla le credenziali e riprova.", "opksshSignInWith": "Accedi con {{provider}}", - "vaultAuthTitle": "Accesso a Vault richiesto", - "vaultAuthDescription": "Una finestra si è aperta per accedere a HashiCorp Vault. Completa l'accesso lì; la connessione continuerà automaticamente.", - "vaultAuthFailed": "Autenticazione Vault fallita. Riprova.", - "vaultReopen": "Riapri la finestra di accesso", "sudoPasswordPopupTitle": "Inserisci password?", - "linkDialogTitle": "Apri collegamento", - "linkDialogOpen": "Apri", - "linkDialogCopy": "Copia", - "websocketAbnormalClose": "La connessione si è chiusa in modo imprevisto. Potrebbe trattarsi di un problema di reverse proxy o configurazione SSL. Controlla i log del server.", "connectionLogTitle": "Log di connessione", "connectionLogCopy": "Copia i log negli appunti", "connectionLogEmpty": "Nessun log di connessione al momento", "connectionLogWaiting": "In attesa dei log di connessione...", "connectionLogCopied": "Log di connessione copiati negli appunti", "connectionLogCopyFailed": "Copia dei log negli appunti fallita", - "connectionRejected": "Connessione rifiutata dal server. Controlla l'autenticazione e la configurazione di rete.", - "hostKeyRejected": "Verifica della chiave host SSH rifiutata. Connessione annullata.", - "sessionTakenOver": "La sessione è stata aperta in un'altra scheda. Riconnessione...", "split": { "splitTab": "Dividi scheda", "addToSplit": "Aggiungi alla divisione", @@ -2225,55 +2161,6 @@ "sectionApiKeys": "Chiavi API", "sectionAuditLog": "Registro di controllo", "sectionSsl": "SSL / Let's Encrypt", - "sectionTouchInput": "Input tattile", - "touchEnabled": "Abilita input tattile", - "touchEnabledDesc": "Consenti lo scorrimento con un solo dito nei terminali", - "touchMomentumEnabled": "Abilitare lo slancio", - "touchMomentumEnabledDesc": "Continua a scorrere brevemente dopo uno swipe", - "touchAdvanced": "Avanzato", - "touchDragThreshold": "Trascinamento", - "touchMaxWheelDelta": "Delta massimo della ruota", - "touchMomentumSampleWindow": "Finestra di campionamento del momento", - "touchReleaseGracePeriod": "Periodo di grazia per il rilascio", - "touchMinimumVelocity": "Velocità minima", - "touchMaximumVelocity": "velocità massima", - "touchMaximumDuration": "Durata massima", - "touchMaximumTravel": "Viaggio massimo", - "touchDecayTime": "tempo di decadimento", - "touchPixelsPerTick": "Pixel per tick", - "touchMaximumFrameInterval": "Intervallo massimo tra i fotogrammi", - "touchMaximumTicksPerFrame": "Numero massimo di tick per fotogramma", - "touchResetDefaults": "Ripristina le impostazioni predefinite", - "touchSaved": "Impostazioni di input touch salvate", - "touchSaveFailed": "Impossibile salvare le impostazioni di input touch.", - "sectionImageStorage": "Archiviazione delle immagini", - "imageStorageMode": "Modalità di archiviazione", - "imageStorageModeDesc": "Dove vengono scritte le immagini del terminale incollate e caricate. Le selezioni automatiche per connessione, locale e remota (SFTP), sono fisse.", - "imageStorageModeAuto": "Auto", - "imageStorageModeLocal": "Locale", - "imageStorageModeRemoteSftp": "Remoto (SFTP)", - "imageStorageLocalDir": "Cartella locale", - "imageStorageLocalDirDesc": "Percorso assoluto nel backend di Termix. Non viene visualizzato dopo il salvataggio per motivi di sicurezza.", - "imageStorageLocalDirPlaceholder": "Lascia vuoto per mantenere aggiornato", - "imageStorageHostPath": "Percorso host", - "imageStorageHostPathDesc": "La visualizzazione lato host della directory locale ricevuta dagli agenti terminali.", - "imageStorageTtl": "TTL (ms)", - "imageStorageMaxCount": "Numero massimo di immagini", - "imageStorageMaxBytes": "byte di archiviazione massimi", - "imageStorageSaved": "Impostazioni di archiviazione delle immagini salvate", - "imageStorageSaveFailed": "Impossibile salvare le impostazioni di archiviazione delle immagini.", - "imageStorageInstanceId": "ID sessione terminale", - "imageStorageInstanceIdDesc": "ID istanza di una delle schede del terminale a cui sei connesso, utilizzato per testare la visibilità.", - "imageStorageInstanceIdRequired": "Inserire prima l'ID dell'istanza della sessione del terminale", - "imageStorageTest": "Test", - "imageStorageTesting": "Test...", - "imageStorageTestFailed": "Impossibile testare l'archiviazione delle immagini", - "imageStorageTestConnected": "La sessione del terminale è connessa.", - "imageStorageTestNotConnected": "Nessuna sessione terminale connessa con questo ID istanza.", - "imageStorageTestSelectedMode": "Per caricare un file si utilizzerebbe: {{mode}}", - "imageStorageSelectedModeUnavailable": "Non disponibile", - "imageStorageTestLocalVisible": "La directory locale è visibile dall'host connesso.", - "imageStorageTestLocalNotVisible": "La directory locale NON è visibile dall'host connesso.", "sectionBranding": "Etichetta bianca", "brandingAppName": "Nome dell'applicazione", "brandingAppNameDesc": "Visualizzato nella schermata di accesso e nella scheda del browser. Lascia vuoto per utilizzare \"Termix\".", @@ -2354,9 +2241,6 @@ "oidcSilentLoginDefaultLockedDesc": "Questa impostazione è bloccata dalla variabile d'ambiente OIDC_SILENT_LOGIN_DEFAULT e non può essere modificata qui.", "allowPasswordReset": "Consenti reset password", "allowPasswordResetDesc": "Codice di reset tramite log Docker", - "commandHistoryEnabled": "Cronologia comandi", - "commandHistoryEnabledDesc": "Consenti la registrazione della cronologia comandi. Se disabilitato, la cronologia non viene salvata indipendentemente dalle impostazioni per host.", - "updateCommandHistoryFailed": "Aggiornamento dell'impostazione della cronologia comandi non riuscito", "analyticsEnabled": "Condividi le statistiche di utilizzo anonime", "analyticsEnabledDesc": "Invia un conteggio giornaliero anonimo di utenti, host e utilizzo delle funzionalità per contribuire al miglioramento di Termix. Non vengono mai inclusi dati personali o dettagli di connessione.", "analyticsEnabledLockedDesc": "Questa impostazione è bloccata dalla variabile d'ambiente ENABLE_TELEMETRY e non può essere modificata qui.", @@ -2647,11 +2531,6 @@ "hostDefaultsTerminal": "Terminale", "hostDefaultsAutoTmux": "Auto-Tmux", "hostDefaultsAutoTmuxDesc": "Eseguire i terminali dei nuovi host all'interno di tmux in modo che le sessioni sopravvivano alle disconnessioni (richiede tmux sull'host).", - "terminalSessionTimeout": "Persistenza della sessione terminale", - "terminalSessionTimeoutDesc": "Per quanto tempo un terminale disconnesso rimane attivo sul server prima di essere chiuso. Minimo 1 minuto · Massimo 1440 minuti. Per le sessioni che devono durare più a lungo, abilitare Auto-Tmux sull'host.", - "terminalSessionTimeoutRange": "Inserisci un valore compreso tra 1 e 1440 minuti", - "terminalSessionTimeoutSaved": "Persistenza della sessione del terminale salvata", - "terminalSessionTimeoutSaveFailed": "Impossibile salvare la persistenza della sessione del terminale.", "minutes": "minuti", "hostDefaultsSessionLogging": "Registrazione sessione", "hostDefaultsSessionLoggingDesc": "Registra sessioni terminale sui nuovi host per impostazione predefinita", @@ -2708,15 +2587,6 @@ "connectToTerminal": "Connetti al terminale", "connectToFiles": "Connetti ai file" }, - "history": { - "noTerminalSelected": "Nessun terminale selezionato", - "noTerminalSelectedHint": "Apri una scheda di terminale SSH per visualizzare la cronologia dei comandi", - "searchPlaceholder": "Cerca nella cronologia...", - "clearAll": "Cancella tutto", - "noHistoryEntries": "Nessuna voce nella cronologia", - "trackingDisabled": "Tracciamento cronologia disabilitato", - "trackingDisabledHint": "Abilitalo nelle impostazioni del terminale dell'host." - }, "sshTools": { "keyRecordingTitle": "Registrazione tasti", "recordToTerminals": "Registra sui terminali", @@ -3178,30 +3048,11 @@ "terminalToolbar": { "title": "Barra degli strumenti del terminale", "showActions": "Mostra la barra degli strumenti", - "hideToolbar": "Nascondi la barra degli strumenti", - "showToolbar": "Espandi la barra degli strumenti", - "moveToolbar": "Sposta la barra degli strumenti", - "moveToolbarHint": "Trascina per riposizionare la barra degli strumenti", - "layout": "Modalità di visualizzazione della barra degli strumenti", - "layoutIcon": "Solo icona", - "layoutLabeled": "Icone ed etichette", - "layoutExpanded": "Espandeto con metriche", - "image": "Azioni sulle immagini (solo immagini)", - "upload": "Carica immagine", - "paste": "Incolla l'immagine", - "detachTmux": "Scollega tmux", - "detachTmuxDescription": "Scollega la sessione tmux", - "imageActionFailed": "L'azione sull'immagine non è riuscita.", - "retry": "Riprova", - "dismissError": "Chiudi errore immagine", "cpu": "processore", "memory": "Memoria", "disk": "Disco", "statsUnavailable": "Statistiche in tempo reale non disponibili", "tmuxDetach": "Scollega la sessione tmux", - "uploadImage": "Carica un'immagine sull'host", - "uploadingImage": "Caricamento in corso...", - "pasteImage": "Incolla l'immagine dagli appunti (solo immagini)", "openFiles": "Apri file", "openDocker": "Open Docker", "openTunnel": "Tunnel aperto", @@ -3285,31 +3136,6 @@ "timeHours": "{{count}}h fa", "timeDays": "{{count}}d fa" }, - "mobileKeyboard": { - "shift": "Maiusc", - "ctrl": "Ctrl", - "esc": "Esc", - "tab": "Tab", - "backTab": "⇥", - "arrowUp": "Freccia Su", - "arrowDown": "Freccia Giù", - "arrowLeft": "Freccia Sinistra", - "arrowRight": "Freccia Destra", - "home": "Home", - "end": "Fine", - "pageUp": "Pag Su", - "pageDown": "Pag Giù", - "delete": "Canc", - "paste": "Impasto", - "editQuickKeys": "Modifica tasti rapidi", - "quickKeysTitle": "Tasti Rapidi", - "quickKeysDesc": "Tocca × per rimuovere. Supporta fino a 8 caratteri.", - "quickKeyPlaceholder": "ad es. sudo ", - "addQuickKey": "Aggiungi", - "removeQuickKey": "Rimuovi", - "resetDefaults": "Ripristina impostazioni predefinite", - "done": "Fine" - }, "serial": { "title": "Seriale", "portLabel": "Porta", diff --git a/src/ui/locales/translated/ja_JP.json b/src/ui/locales/translated/ja_JP.json index 0ff61b06f..9d1855481 100644 --- a/src/ui/locales/translated/ja_JP.json +++ b/src/ui/locales/translated/ja_JP.json @@ -742,7 +742,6 @@ "addHost": "ホストを追加", "editHost": "ホストを編集", "cloneHost": "ホストを複製", - "enableTerminal": "ターミナルを有効にする", "enableTunnel": "トンネルを有効にする", "enableFileManager": "ファイルマネージャーを有効にする", "enableDocker": "Dockerを有効にする", @@ -1041,8 +1040,6 @@ "enableSessionLoggingDesc": "後で確認できるようにターミナルセッションの出力を記録します", "allowSessionSharing": "セッション共有を許可する", "allowSessionSharingDesc": "このホスト上のライブセッションをリンクまたは他のユーザーと共有できるようにします。", - "enableCommandHistory": "コマンド履歴", - "enableCommandHistoryDesc": "このターミナルで実行されたコマンドを履歴とオートコンプリート用に記録します", "localEchoLabel": "ローカルエコー", "localEchoDesc": "低速回線でも、印刷可能な安全なキー入力をローカルで予測します。パスワード入力や制御入力はエコーバックされません。", "localEchoDefault": "グローバル設定を使用する", @@ -1072,8 +1069,6 @@ "bindHost": "バインドホスト", "enableTmuxMonitor": "Tmuxモニターを有効にする", "enableTmuxMonitorDesc": "Tmuxモニターにこのホストを表示し、tmuxアクションをサイドバーに追加します。", - "enableTerminalToolbar": "ターミナルツールバーを有効にする", - "enableTerminalToolbarDesc": "このホストのツールとリアルタイム統計情報に素早くアクセスできるツールバーを、アクティブなターミナルにドッキングして表示します。", "connectionToolbar": "接続ツールバー", "showConnectionToolbar": "接続ツールバーを表示する", "showConnectionToolbarDesc": "このホストのSSH、RDP、VNC、およびTelnetセッションでツールバーを表示します。ツールバー自体から一時的に非表示にすることもできます。", @@ -1550,9 +1545,7 @@ "links": "リンク", "navigate": "移動", "select": "選択", - "toggleWith": "切り替え", - "localTerminal": "ローカル端末", - "localTerminalDesc": "このコンピュータでシェルを開く" + "toggleWith": "切り替え" }, "splitScreen": { "paneEmpty": "ペイン {{index}} - 空", @@ -1717,57 +1710,13 @@ "loadSharesFailed": "アクティブな共有の読み込みに失敗しました" }, "terminal": { - "sessionExpiredNotice": "{{host}} での前回のセッションは {{minutes}} 分後に切断され、シェルおよびそこで実行されていたすべてのものが失われました。セッションを維持するには、このホストで Auto-Tmux を有効にしてください。", - "sessionExpiredNoticeNoMinutes": "{{host}} での前回のセッションは、接続が切断された状態で期限切れになりました。シェルおよびシェル内で実行されていたすべてのプロセスは失われています。セッションを維持するには、このホストで Auto-Tmux を有効にしてください。", - "enableAutoTmuxAction": "Auto-Tmuxを有効にする", - "autoTmuxEnabled": "{{host}}に対して Auto-Tmux が有効になりました。これは次回の接続時に適用されます。", - "autoTmuxEnableFailed": "Auto-Tmux の有効化に失敗しました", "connect": "ホストに接続", "clear": "クリア", "paste": "貼り付け", - "reconnect": "再接続", - "connectionLost": "接続切断", "connected": "接続中", "clipboardWriteFailed": "クリップボードへのコピーに失敗しました。ページがHTTPSまたはlocalhostで提供されていることを確認してください。", "clipboardReadFailed": "クリップボードからの読み取りに失敗しました。クリップボードのアクセス許可が付与されていることを確認してください。", - "clipboardHttpWarning": "クリップボードを読み取れませんでした。一部のブラウザはHTTPS接続以外でのクリップボードへのアクセスを制限しています。代わりにCtrl+Vを試すか、TermixをHTTPS経由で提供してみてください。", - "passwordPromptFillTitle": "保存されたパスワードをこのプロンプトに入力しますか?", - "unknownError": "不明なエラーが発生しました", - "websocketError": "WebSocket接続エラー", - "connecting": "接続中...", - "noHostSelected": "ホストが選択されていません", - "reconnecting": "再接続中... ({{attempt}}/{{max}})", - "reconnected": "再接続完了", - "tmuxSessionCreated": "tmuxセッションが作成されました: {{name}}", - "tmuxSessionAttached": "tmuxセッションに接続しました: {{name}}", - "tmuxUnavailable": "リモートホストにtmuxがインストールされていません。標準シェルにフォールバックします。", - "tmuxSessionPickerTitle": "tmuxセッション", - "tmuxSessionPickerDesc": "このホストに既存のtmuxセッションがあります。再アタッチするか、新しいセッションを作成してください。", - "tmuxWindows": "ウィンドウ", - "tmuxWindowCount": "{{count}} ウィンドウ", - "tmuxAttached": "接続クライアント", - "tmuxAttachedCount": "{{count}} 接続", - "tmuxLastActivity": "最終アクティビティ", - "tmuxTimeJustNow": "たった今", - "tmuxTimeMinutes": "{{count}}分前", - "tmuxTimeHours": "{{count}}時間前", - "tmuxTimeDays": "{{count}}日前", - "tmuxCreateNew": "新規セッションを開始", - "tmuxCopyHint": "選択範囲を調整してEnterでクリップボードにコピー", "tmuxDetach": "tmuxセッションからデタッチ", - "tmuxDetached": "tmuxセッションからデタッチしました", - "searchPlaceholder": "探す", - "searchCaseSensitive": "マッチケース", - "searchWholeWord": "単語全体を一致させる", - "searchRegex": "正規表現を使用する", - "searchNoResults": "検索結果なし", - "searchResultCount": "{{index}} / {{count}}", - "searchNext": "次の試合(入場)", - "searchPrevious": "前の試合 (Shift+Enter)", - "searchClose": "閉じる(Escキー)", - "maxReconnectAttemptsReached": "再接続の最大試行回数に達しました", - "closeTab": "閉じる", - "connectionTimeout": "接続タイムアウト", "terminalTitle": "ターミナル - {{host}}", "terminalWithPath": "ターミナル - {{host}}:{{path}}", "runTitle": "{{command}} 実行中 - {{host}}", @@ -1790,27 +1739,14 @@ "opksshOpenBrowser": "ブラウザを開いて認証", "opksshWaitingForAuth": "ブラウザでの認証を待機中...", "opksshAuthenticating": "認証を処理中...", - "opksshTimeout": "認証がタイムアウトしました。もう一度お試しください。", - "opksshAuthFailed": "認証に失敗しました。資格情報を確認して再試行してください。", "opksshSignInWith": "{{provider}} でサインイン", - "vaultAuthTitle": "Vault サインインが必要です", - "vaultAuthDescription": "HashiCorp Vaultへのサインイン用のウィンドウが開きました。そこでサインインを完了すると、この接続は自動的に続行されます。", - "vaultAuthFailed": "Vault 認証に失敗しました。もう一度お試しください。", - "vaultReopen": "サインインウィンドウを再表示", "sudoPasswordPopupTitle": "パスワードを入力しますか?", - "linkDialogTitle": "リンクを開く", - "linkDialogOpen": "開く", - "linkDialogCopy": "コピー", - "websocketAbnormalClose": "接続が予期せず切断されました。リバースプロキシまたはSSLの設定に問題がある可能性があります。サーバーログを確認してください。", "connectionLogTitle": "接続ログ", "connectionLogCopy": "ログをクリップボードにコピー", "connectionLogEmpty": "まだ接続ログはありません", "connectionLogWaiting": "接続ログを待機中...", "connectionLogCopied": "接続ログがクリップボードにコピーされました", "connectionLogCopyFailed": "ログのクリップボードへのコピーに失敗しました", - "connectionRejected": "サーバーによって接続が拒否されました。認証設定とネットワーク構成を確認してください。", - "hostKeyRejected": "SSHホストキーの検証が拒否されました。接続がキャンセルされました。", - "sessionTakenOver": "セッションが別のタブで開かれました。再接続中...", "split": { "splitTab": "タブを分割", "addToSplit": "分割に追加", @@ -2225,55 +2161,6 @@ "sectionApiKeys": "APIキー", "sectionAuditLog": "監査ログ", "sectionSsl": "SSL / Let's Encrypt", - "sectionTouchInput": "タッチ入力", - "touchEnabled": "タッチ入力を有効にする", - "touchEnabledDesc": "端末で指一本でのスクロールを可能にする", - "touchMomentumEnabled": "勢いを加速させる", - "touchMomentumEnabledDesc": "スワイプ後、少しスクロールを続ける", - "touchAdvanced": "高度な", - "touchDragThreshold": "ドラッグしきい値", - "touchMaxWheelDelta": "最大ホイールデルタ", - "touchMomentumSampleWindow": "モメンタムサンプルウィンドウ", - "touchReleaseGracePeriod": "釈放猶予期間", - "touchMinimumVelocity": "最小速度", - "touchMaximumVelocity": "最大速度", - "touchMaximumDuration": "最大持続時間", - "touchMaximumTravel": "最大移動距離", - "touchDecayTime": "減衰時間", - "touchPixelsPerTick": "ピクセル/ティック", - "touchMaximumFrameInterval": "最大フレーム間隔", - "touchMaximumTicksPerFrame": "フレームあたりの最大ティック数", - "touchResetDefaults": "初期設定に戻す", - "touchSaved": "タッチ入力設定が保存されました", - "touchSaveFailed": "タッチ入力設定の保存に失敗しました", - "sectionImageStorage": "画像ストレージ", - "imageStorageMode": "ストレージモード", - "imageStorageModeDesc": "貼り付けおよびアップロードされた端末イメージが書き込まれる場所。接続による自動選択、ローカルおよびリモート(SFTP)が固定されています。", - "imageStorageModeAuto": "自動", - "imageStorageModeLocal": "地元", - "imageStorageModeRemoteSftp": "リモート(SFTP)", - "imageStorageLocalDir": "ローカルディレクトリ", - "imageStorageLocalDirDesc": "Termixバックエンド上の絶対パス。セキュリティ上の理由から、保存後は表示されません。", - "imageStorageLocalDirPlaceholder": "最新情報を維持するには空欄のままにしてください", - "imageStorageHostPath": "ホストパス", - "imageStorageHostPathDesc": "端末エージェントが受信するローカルディレクトリのホスト側ビュー。", - "imageStorageTtl": "TTL(ミリ秒)", - "imageStorageMaxCount": "最大画像数", - "imageStorageMaxBytes": "最大ストレージバイト数", - "imageStorageSaved": "画像保存設定が保存されました", - "imageStorageSaveFailed": "画像保存設定の保存に失敗しました", - "imageStorageInstanceId": "端末セッションインスタンスID", - "imageStorageInstanceIdDesc": "表示されている可視性をテストするために使用される、接続されているターミナルタブのいずれかのインスタンスID。", - "imageStorageInstanceIdRequired": "まず、ターミナルセッションインスタンスIDを入力してください。", - "imageStorageTest": "テスト", - "imageStorageTesting": "テスト中...", - "imageStorageTestFailed": "画像ストレージのテストに失敗しました", - "imageStorageTestConnected": "端末セッションが接続されました。", - "imageStorageTestNotConnected": "このインスタンスIDで接続されている端末セッションはありません。", - "imageStorageTestSelectedMode": "アップロードには以下を使用します: {{mode}}", - "imageStorageSelectedModeUnavailable": "利用不可", - "imageStorageTestLocalVisible": "ローカルディレクトリは、接続されているホストから参照可能です。", - "imageStorageTestLocalNotVisible": "ローカルディレクトリは、接続されているホストからアクセスできません。", "sectionBranding": "ホワイトラベル", "brandingAppName": "アプリケーション名", "brandingAppNameDesc": "ログイン画面とブラウザタブに表示されます。「Termix」を使用する場合は空欄のままにしてください。", @@ -2354,9 +2241,6 @@ "oidcSilentLoginDefaultLockedDesc": "この設定は環境変数 OIDC_SILENT_LOGIN_DEFAULT によってロックされているため、ここで変更することはできません。", "allowPasswordReset": "パスワードリセットを許可", "allowPasswordResetDesc": "リセットコードをDockerログから取得", - "commandHistoryEnabled": "コマンド履歴", - "commandHistoryEnabledDesc": "コマンド履歴の記録を許可します。無効にすると、ホストごとの設定に関わらず履歴は保存されません。", - "updateCommandHistoryFailed": "コマンド履歴設定の更新に失敗しました", "analyticsEnabled": "匿名利用統計を共有する", "analyticsEnabledDesc": "Termixの改善に役立てるため、ユーザー数、ホスト数、機能使用状況に関する匿名データを毎日送信します。個人データや接続情報は一切含まれません。", "analyticsEnabledLockedDesc": "この設定は環境変数ENABLE_TELEMETRYによってロックされているため、ここで変更することはできません。", @@ -2647,11 +2531,6 @@ "hostDefaultsTerminal": "ターミナル", "hostDefaultsAutoTmux": "Auto-Tmux", "hostDefaultsAutoTmuxDesc": "tmux内で新しいホストのターミナルを実行することで、セッションが切断後も維持されます(ホストにtmuxがインストールされている必要があります)。", - "terminalSessionTimeout": "端末セッションの永続性", - "terminalSessionTimeoutDesc": "切断された端末がサーバー上で閉じられるまでの時間。最小1分、最大1440分。この時間を超えてセッションを継続する必要がある場合は、ホスト上でAuto-Tmuxを有効にしてください。", - "terminalSessionTimeoutRange": "1~1440分の間の値を入力してください。", - "terminalSessionTimeoutSaved": "端末セッションの永続性が保存されました", - "terminalSessionTimeoutSaveFailed": "端末セッションの永続化を保存できませんでした", "minutes": "分", "hostDefaultsSessionLogging": "セッションロギング", "hostDefaultsSessionLoggingDesc": "デフォルトで新規ホストのターミナルセッションを記録する", @@ -2708,15 +2587,6 @@ "connectToTerminal": "ターミナルに接続", "connectToFiles": "ファイルに接続" }, - "history": { - "noTerminalSelected": "ターミナルが選択されていません", - "noTerminalSelectedHint": "SSH ターミナルタブを開いてコマンド履歴を表示", - "searchPlaceholder": "履歴を検索...", - "clearAll": "すべてクリア", - "noHistoryEntries": "履歴エントリがありません", - "trackingDisabled": "履歴の追跡が無効です", - "trackingDisabledHint": "ホストのターミナル設定で有効にしてください。" - }, "sshTools": { "keyRecordingTitle": "キー記録", "recordToTerminals": "ターミナルに記録", @@ -3178,30 +3048,11 @@ "terminalToolbar": { "title": "ターミナルツールバー", "showActions": "ツールバーを表示", - "hideToolbar": "ツールバーを非表示にする", - "showToolbar": "ツールバーを展開する", - "moveToolbar": "ツールバーを移動", - "moveToolbarHint": "ツールバーをドラッグして位置を変更します", - "layout": "ツールバー表示モード", - "layoutIcon": "アイコンのみ", - "layoutLabeled": "アイコンとラベル", - "layoutExpanded": "指標を追加して拡張", - "image": "画像アクション(画像のみ)", - "upload": "画像をアップロード", - "paste": "画像を貼り付ける", - "detachTmux": "tmuxをデタッチする", - "detachTmuxDescription": "tmuxセッションをデタッチする", - "imageActionFailed": "画像操作が失敗しました", - "retry": "リトライ", - "dismissError": "画像エラーを非表示にする", "cpu": "CPU", "memory": "メモリ", "disk": "ディスク", "statsUnavailable": "ライブ統計は利用できません", "tmuxDetach": "tmuxセッションをデタッチする", - "uploadImage": "ホストに画像をアップロードする", - "uploadingImage": "アップロード中...", - "pasteImage": "クリップボードから画像を貼り付ける(画像のみ)", "openFiles": "ファイルを開く", "openDocker": "Open Docker", "openTunnel": "オープントンネル", @@ -3285,31 +3136,6 @@ "timeHours": "{{count}}時間前", "timeDays": "{{count}}日前" }, - "mobileKeyboard": { - "shift": "Shift", - "ctrl": "Ctrl", - "esc": "Esc", - "tab": "Tab", - "backTab": "⇥", - "arrowUp": "↑", - "arrowDown": "↓", - "arrowLeft": "←", - "arrowRight": "→", - "home": "ホーム", - "end": "エンド", - "pageUp": "PgUp", - "pageDown": "PgDn", - "delete": "Del", - "paste": "ペースト", - "editQuickKeys": "クイックキー編集", - "quickKeysTitle": "クイックキー", - "quickKeysDesc": "× をタップして削除します。最大8文字まで対応。", - "quickKeyPlaceholder": "例: sudo ", - "addQuickKey": "追加", - "removeQuickKey": "削除", - "resetDefaults": "デフォルトにリセット", - "done": "完了" - }, "serial": { "title": "シリアル", "portLabel": "ポート", diff --git a/src/ui/locales/translated/ko_KR.json b/src/ui/locales/translated/ko_KR.json index 68248b3a0..27a2b9957 100644 --- a/src/ui/locales/translated/ko_KR.json +++ b/src/ui/locales/translated/ko_KR.json @@ -742,7 +742,6 @@ "addHost": "호스트 추가", "editHost": "호스트 편집", "cloneHost": "호스트 복제", - "enableTerminal": "터미널 활성화", "enableTunnel": "터널 활성화", "enableFileManager": "파일 관리자 활성화", "enableDocker": "Docker 활성화", @@ -1041,8 +1040,6 @@ "enableSessionLoggingDesc": "나중에 검토할 수 있도록 터미널 세션 출력을 기록합니다", "allowSessionSharing": "세션 공유 허용", "allowSessionSharingDesc": "이 호스트에서 진행되는 라이브 세션을 링크 또는 다른 사용자와 공유하세요.", - "enableCommandHistory": "명령어 히스토리", - "enableCommandHistoryDesc": "히스토리 및 자동 완성을 위해 이 터미널에서 실행된 명령어를 기록합니다", "localEchoLabel": "로컬 에코", "localEchoDesc": "연결 속도가 느린 환경에서도 안전하게 입력 가능한 키 입력을 로컬에서 예측합니다. 비밀번호 입력 및 제어 입력은 절대 화면에 표시되지 않습니다.", "localEchoDefault": "전역 설정을 사용하세요", @@ -1072,8 +1069,6 @@ "bindHost": "바인드 호스트", "enableTmuxMonitor": "Tmux 모니터 활성화", "enableTmuxMonitorDesc": "Tmux 모니터에 이 호스트를 표시하고 tmux 작업을 사이드바에 추가합니다.", - "enableTerminalToolbar": "터미널 툴바 활성화", - "enableTerminalToolbarDesc": "현재 활성화된 터미널에 도킹된 빠른 액세스 툴바를 표시하여 해당 호스트의 도구 및 실시간 통계를 볼 수 있도록 합니다.", "connectionToolbar": "연결 도구 모음", "showConnectionToolbar": "연결 도구 모음 표시", "showConnectionToolbarDesc": "이 호스트의 SSH, RDP, VNC 및 Telnet 세션에 툴바를 표시합니다. 툴바 자체에서 툴바를 일시적으로 숨길 수도 있습니다.", @@ -1550,9 +1545,7 @@ "links": "링크", "navigate": "이동", "select": "선택", - "toggleWith": "전환 키:", - "localTerminal": "로컬 터미널", - "localTerminalDesc": "이 컴퓨터에서 셸을 엽니다." + "toggleWith": "전환 키:" }, "splitScreen": { "paneEmpty": "분할 창 {{index}} - 비어 있음", @@ -1717,57 +1710,13 @@ "loadSharesFailed": "활성 공유 항목을 로드하는 데 실패했습니다." }, "terminal": { - "sessionExpiredNotice": "{{host}} 호스트에서 진행 중이던 이전 세션이 {{minutes}} 분 후 연결이 끊어지면서 만료되었습니다. 셸과 셸에서 실행 중이던 모든 항목이 종료되었습니다. 세션을 유지하려면 이 호스트에서 자동 Tmux를 활성화하십시오.", - "sessionExpiredNoticeNoMinutes": "{{host}} 에서 이전 세션이 연결이 끊긴 상태에서 만료되었습니다. 셸과 그 안에서 실행 중이던 모든 항목이 사라졌습니다. 세션을 유지하려면 이 호스트에서 자동 Tmux를 활성화하십시오.", - "enableAutoTmuxAction": "자동 Tmux 활성화", - "autoTmuxEnabled": "{{host}}에 대해 자동 Tmux가 활성화되었습니다. 이는 다음 연결부터 적용됩니다.", - "autoTmuxEnableFailed": "자동 Tmux를 활성화하는 데 실패했습니다.", "connect": "호스트에 연결", "clear": "지우기", "paste": "붙여넣기", - "reconnect": "재연결", - "connectionLost": "연결이 끊어졌습니다", "connected": "연결됨", "clipboardWriteFailed": "클립보드에 복사하지 못했습니다. 페이지가 HTTPS 또는 localhost에서 제공되는지 확인하세요.", "clipboardReadFailed": "클립보드에서 읽지 못했습니다. 클립보드 권한이 허용되었는지 확인하세요.", - "clipboardHttpWarning": "클립보드를 읽을 수 없습니다. 일부 브라우저는 HTTPS 외부에서 클립보드 접근을 제한합니다. Ctrl+V를 눌러 V를 누르거나 Termix를 HTTPS로 실행해 보세요.", - "passwordPromptFillTitle": "저장된 비밀번호를 이 프롬프트에 입력하시겠습니까?", - "unknownError": "알 수 없는 오류가 발생했습니다.", - "websocketError": "WebSocket 연결 오류", - "connecting": "연결 중...", - "noHostSelected": "선택된 호스트가 없습니다.", - "reconnecting": "재연결 중... ({{attempt}}/{{max}})", - "reconnected": "재연결 성공", - "tmuxSessionCreated": "tmux 세션 생성됨: {{name}}", - "tmuxSessionAttached": "tmux 세션 연결됨: {{name}}", - "tmuxUnavailable": "원격 호스트에 tmux가 설치되어 있지 않아 표준 셸로 대체합니다.", - "tmuxSessionPickerTitle": "tmux 세션", - "tmuxSessionPickerDesc": "이 호스트에서 기존 tmux 세션을 찾았습니다. 다시 연결할 세션을 선택하거나 새 세션을 만드세요.", - "tmuxWindows": "윈도우", - "tmuxWindowCount": "{{count}}개 윈도우", - "tmuxAttached": "연결된 클라이언트", - "tmuxAttachedCount": "{{count}}개 연결됨", - "tmuxLastActivity": "마지막 활동", - "tmuxTimeJustNow": "방금", - "tmuxTimeMinutes": "{{count}}분 전", - "tmuxTimeHours": "{{count}}시간 전", - "tmuxTimeDays": "{{count}}일 전", - "tmuxCreateNew": "새 세션 시작", - "tmuxCopyHint": "선택 영역을 조정하고 Enter 키를 누르면 클립보드에 복사됩니다.", "tmuxDetach": "tmux 세션에서 분리", - "tmuxDetached": "tmux 세션에서 분리됨", - "searchPlaceholder": "찾다", - "searchCaseSensitive": "대소문자를 일치시키세요", - "searchWholeWord": "단어 전체를 일치시키세요", - "searchRegex": "정규 표현식을 사용하세요", - "searchNoResults": "결과가 없습니다", - "searchResultCount": "{{index}} {{count}}", - "searchNext": "다음 매치 (Enter)", - "searchPrevious": "이전 일치 항목 (Shift+Enter)", - "searchClose": "닫기 (탈출)", - "maxReconnectAttemptsReached": "최대 재연결 시도 횟수에 도달했습니다.", - "closeTab": "닫기", - "connectionTimeout": "연결 시간 초과", "terminalTitle": "터미널 - {{host}}", "terminalWithPath": "터미널 - {{host}}:{{path}}", "runTitle": "{{command}} 실행 중 - {{host}}", @@ -1790,27 +1739,14 @@ "opksshOpenBrowser": "브라우저로 인증하기", "opksshWaitingForAuth": "브라우저에서 인증 대기 중...", "opksshAuthenticating": "인증 처리 중...", - "opksshTimeout": "인증 시간이 초과되었습니다. 다시 시도하세요.", - "opksshAuthFailed": "인증에 실패했습니다. 자격 증명을 확인하고 다시 시도하세요.", "opksshSignInWith": "{{provider}}로 로그인", - "vaultAuthTitle": "Vault 로그인 필요", - "vaultAuthDescription": "HashiCorp Vault 로그인을 위한 창이 열렸습니다. 해당 창에서 로그인을 완료하면 연결이 자동으로 계속됩니다.", - "vaultAuthFailed": "Vault 인증에 실패했습니다. 다시 시도하세요.", - "vaultReopen": "로그인 창 다시 열기", "sudoPasswordPopupTitle": "비밀번호 입력", - "linkDialogTitle": "링크 열기", - "linkDialogOpen": "열기", - "linkDialogCopy": "복사", - "websocketAbnormalClose": "연결이 예기치 않게 종료되었습니다. 리버스 프록시 또는 SSL 구성 문제일 수 있습니다. 서버 로그를 확인하세요.", "connectionLogTitle": "연결 로그", "connectionLogCopy": "로그를 클립보드에 복사", "connectionLogEmpty": "아직 연결 로그가 없습니다", "connectionLogWaiting": "연결 로그를 기다리는 중...", "connectionLogCopied": "연결 로그가 클립보드에 복사되었습니다", "connectionLogCopyFailed": "로그를 클립보드에 복사하지 못했습니다", - "connectionRejected": "서버에서 연결이 거부되었습니다. 인증 및 네트워크 구성을 확인하세요.", - "hostKeyRejected": "SSH 호스트 키 검증이 거부되었습니다. 연결이 취소되었습니다.", - "sessionTakenOver": "다른 탭에서 세션이 열렸습니다. 다시 연결 중...", "split": { "splitTab": "탭 분할", "addToSplit": "분할에 추가", @@ -2225,55 +2161,6 @@ "sectionApiKeys": "API 키", "sectionAuditLog": "감사 로그", "sectionSsl": "SSL / Let's Encrypt", - "sectionTouchInput": "터치 입력", - "touchEnabled": "터치 입력 활성화", - "touchEnabledDesc": "단말기에서 한 손가락 터치 스크롤을 허용합니다.", - "touchMomentumEnabled": "모멘텀을 활성화하세요", - "touchMomentumEnabledDesc": "스와이프 후 잠시 스크롤을 더 내리세요.", - "touchAdvanced": "고급의", - "touchDragThreshold": "드래그 임계값", - "touchMaxWheelDelta": "최대 휠 델타", - "touchMomentumSampleWindow": "모멘텀 샘플 윈도우", - "touchReleaseGracePeriod": "릴리스 유예 기간", - "touchMinimumVelocity": "최소 속도", - "touchMaximumVelocity": "최대 속도", - "touchMaximumDuration": "최대 지속 시간", - "touchMaximumTravel": "최대 이동", - "touchDecayTime": "소멸 시간", - "touchPixelsPerTick": "틱당 픽셀", - "touchMaximumFrameInterval": "최대 프레임 간격", - "touchMaximumTicksPerFrame": "프레임당 최대 틱 수", - "touchResetDefaults": "기본 설정으로 재설정", - "touchSaved": "터치 입력 설정이 저장되었습니다.", - "touchSaveFailed": "터치 입력 설정을 저장하는 데 실패했습니다.", - "sectionImageStorage": "이미지 저장", - "imageStorageMode": "저장 모드", - "imageStorageModeDesc": "붙여넣거나 업로드한 터미널 이미지가 표시되는 위치입니다. 연결 유형에 따라 로컬 및 원격(SFTP) 이미지가 자동으로 선택됩니다.", - "imageStorageModeAuto": "자동", - "imageStorageModeLocal": "현지의", - "imageStorageModeRemoteSftp": "원격(SFTP)", - "imageStorageLocalDir": "로컬 디렉토리", - "imageStorageLocalDirDesc": "Termix 백엔드에서의 절대 경로입니다. 보안상의 이유로 저장 후에는 표시되지 않습니다.", - "imageStorageLocalDirPlaceholder": "현재 상태를 유지하려면 비워 두세요.", - "imageStorageHostPath": "호스트 경로", - "imageStorageHostPathDesc": "터미널 에이전트가 수신하는 로컬 디렉터리의 호스트 측 보기입니다.", - "imageStorageTtl": "TTL(밀리초)", - "imageStorageMaxCount": "최대 이미지 개수", - "imageStorageMaxBytes": "최대 저장 용량(바이트)", - "imageStorageSaved": "이미지 저장 설정이 저장되었습니다.", - "imageStorageSaveFailed": "이미지 저장 설정 저장에 실패했습니다.", - "imageStorageInstanceId": "터미널 세션 인스턴스 ID", - "imageStorageInstanceIdDesc": "연결된 터미널 탭 중 하나의 인스턴스 ID입니다. 이는 가시성 테스트에 사용됩니다.", - "imageStorageInstanceIdRequired": "먼저 터미널 세션 인스턴스 ID를 입력하십시오.", - "imageStorageTest": "시험", - "imageStorageTesting": "테스트 중...", - "imageStorageTestFailed": "이미지 저장소 테스트에 실패했습니다.", - "imageStorageTestConnected": "터미널 세션이 연결되었습니다.", - "imageStorageTestNotConnected": "이 인스턴스 ID로 연결된 터미널 세션이 없습니다.", - "imageStorageTestSelectedMode": "업로드 시에는 {{mode}}를 사용합니다.", - "imageStorageSelectedModeUnavailable": "없는", - "imageStorageTestLocalVisible": "로컬 디렉터리는 연결된 호스트에서 볼 수 있습니다.", - "imageStorageTestLocalNotVisible": "로컬 디렉터리는 연결된 호스트에서 볼 수 없습니다.", "sectionBranding": "화이트 라벨", "brandingAppName": "애플리케이션 이름", "brandingAppNameDesc": "로그인 화면과 브라우저 탭에 표시됩니다. \"Termix\"를 사용하려면 비워 두십시오.", @@ -2354,9 +2241,6 @@ "oidcSilentLoginDefaultLockedDesc": "이 설정은 OIDC_SILENT_LOGIN_DEFAULT 환경 변수에 의해 잠겨 있으므로 여기에서 변경할 수 없습니다.", "allowPasswordReset": "비밀번호 재설정 허용", "allowPasswordResetDesc": "Docker 로그를 통해 재설정 코드 확인", - "commandHistoryEnabled": "명령어 히스토리", - "commandHistoryEnabledDesc": "명령어 히스토리 기록 허용. 비활성화 시 호스트별 설정과 무관하게 히스토리가 저장되지 않습니다.", - "updateCommandHistoryFailed": "명령어 히스토리 설정 업데이트 실패", "analyticsEnabled": "익명 사용 통계 공유", "analyticsEnabledDesc": "Termix 개선을 위해 사용자, 호스트 및 기능 사용량에 대한 익명의 일일 통계를 전송합니다. 개인 정보나 연결 정보는 절대 포함되지 않습니다.", "analyticsEnabledLockedDesc": "이 설정은 ENABLE_TELEMETRY 환경 변수에 의해 잠겨 있으므로 여기에서 변경할 수 없습니다.", @@ -2647,11 +2531,6 @@ "hostDefaultsTerminal": "터미널", "hostDefaultsAutoTmux": "자동 Tmux", "hostDefaultsAutoTmuxDesc": "새 호스트의 터미널을 tmux 내에서 실행하여 연결이 끊어져도 세션이 유지되도록 합니다(호스트에 tmux가 설치되어 있어야 함).", - "terminalSessionTimeout": "터미널 세션 지속성", - "terminalSessionTimeoutDesc": "연결이 끊긴 터미널이 서버에서 종료되기 전까지 유지되는 시간입니다. 최소 1분, 최대 1440분. 이 시간보다 오래 세션을 유지해야 하는 경우 호스트에서 자동 Tmux를 활성화하십시오.", - "terminalSessionTimeoutRange": "1분에서 1440분 사이의 값을 입력하세요.", - "terminalSessionTimeoutSaved": "터미널 세션이 저장되었습니다.", - "terminalSessionTimeoutSaveFailed": "터미널 세션 지속성 저장에 실패했습니다.", "minutes": "분", "hostDefaultsSessionLogging": "세션 로깅", "hostDefaultsSessionLoggingDesc": "기본적으로 새 호스트에서 터미널 세션 기록", @@ -2708,15 +2587,6 @@ "connectToTerminal": "터미널에 연결", "connectToFiles": "파일에 연결" }, - "history": { - "noTerminalSelected": "선택된 터미널 없음", - "noTerminalSelectedHint": "SSH 터미널 탭을 열어 명령 기록을 확인하세요.", - "searchPlaceholder": "기록 검색...", - "clearAll": "모두 지우기", - "noHistoryEntries": "기록 항목 없음", - "trackingDisabled": "기록 추적이 비활성화됨", - "trackingDisabledHint": "호스트의 터미널 설정에서 활성화하세요." - }, "sshTools": { "keyRecordingTitle": "키 기록", "recordToTerminals": "터미널에 기록", @@ -3178,30 +3048,11 @@ "terminalToolbar": { "title": "터미널 툴바", "showActions": "툴바 표시", - "hideToolbar": "툴바 숨기기", - "showToolbar": "확장 도구 모음", - "moveToolbar": "툴바 이동", - "moveToolbarHint": "드래그하여 툴바의 위치를 변경하세요.", - "layout": "툴바 표시 모드", - "layoutIcon": "아이콘만", - "layoutLabeled": "아이콘 및 레이블", - "layoutExpanded": "측정 지표를 추가하여 확장했습니다.", - "image": "이미지 관련 작업 (이미지에만 해당)", - "upload": "이미지 업로드", - "paste": "이미지 붙여넣기", - "detachTmux": "tmux 분리", - "detachTmuxDescription": "tmux 세션 분리", - "imageActionFailed": "이미지 작업이 실패했습니다.", - "retry": "다시 해 보다", - "dismissError": "이미지 오류를 닫습니다", "cpu": "CPU", "memory": "메모리", "disk": "디스크", "statsUnavailable": "실시간 통계를 사용할 수 없습니다.", "tmuxDetach": "tmux 세션 분리", - "uploadImage": "호스트에 이미지를 업로드하세요", - "uploadingImage": "업로드 중...", - "pasteImage": "클립보드에서 이미지 붙여넣기 (이미지만 해당)", "openFiles": "열려있는 파일", "openDocker": "오픈 도커", "openTunnel": "열린 터널", @@ -3285,31 +3136,6 @@ "timeHours": "{{count}}시간 전", "timeDays": "{{count}}일 전" }, - "mobileKeyboard": { - "shift": "Shift", - "ctrl": "Ctrl", - "esc": "Esc", - "tab": "Tab", - "backTab": "⇥", - "arrowUp": "위쪽 화살표", - "arrowDown": "아래쪽 화살표", - "arrowLeft": "왼쪽 화살표", - "arrowRight": "오른쪽 화살표", - "home": "Home", - "end": "End", - "pageUp": "PgUp", - "pageDown": "PgDn", - "delete": "Del", - "paste": "반죽", - "editQuickKeys": "빠른 키 편집", - "quickKeysTitle": "빠른 키", - "quickKeysDesc": "×를 탭하여 제거합니다. 최대 8자까지 지원합니다.", - "quickKeyPlaceholder": "예: sudo ", - "addQuickKey": "추가", - "removeQuickKey": "제거", - "resetDefaults": "기본값으로 초기화", - "done": "완료" - }, "serial": { "title": "시리얼", "portLabel": "포트", diff --git a/src/ui/locales/translated/nl_NL.json b/src/ui/locales/translated/nl_NL.json index 15332bcc7..94e5b8c37 100644 --- a/src/ui/locales/translated/nl_NL.json +++ b/src/ui/locales/translated/nl_NL.json @@ -742,7 +742,6 @@ "addHost": "Voeg een host toe", "editHost": "Bewerk de host", "cloneHost": "Kloonhost", - "enableTerminal": "Terminal inschakelen", "enableTunnel": "Tunnel inschakelen", "enableFileManager": "Bestandsbeheer inschakelen", "enableDocker": "Docker inschakelen", @@ -1041,8 +1040,6 @@ "enableSessionLoggingDesc": "Sla de uitvoer van de terminalsessie op voor latere beoordeling.", "allowSessionSharing": "Sessiedeling toestaan", "allowSessionSharingDesc": "Sta toe dat live sessies op deze host via een link of met andere gebruikers worden gedeeld.", - "enableCommandHistory": "Commandogeschiedenis", - "enableCommandHistoryDesc": "Sla de commando's die in deze terminal worden uitgevoerd op voor de geschiedenis en automatisch aanvullen.", "localEchoLabel": "Local Echo", "localEchoDesc": "Voorspel lokaal veilige toetsaanslagen, ook bij trage internetverbindingen. Wachtwoordprompts en besturingsinvoer worden nooit weergegeven.", "localEchoDefault": "Gebruik de globale instelling", @@ -1072,8 +1069,6 @@ "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.", - "enableTerminalToolbar": "Terminalwerkbalk inschakelen", - "enableTerminalToolbarDesc": "Toon een sneltoegangsbalk die is vastgezet aan de actieve terminal voor de tools en live statistieken van deze host.", "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.", @@ -1550,9 +1545,7 @@ "links": "Links", "navigate": "Navigeren", "select": "Select", - "toggleWith": "Schakelen met", - "localTerminal": "Lokale terminal", - "localTerminalDesc": "Open een shell op deze computer." + "toggleWith": "Schakelen met" }, "splitScreen": { "paneEmpty": "Paneel {{index}} - leeg", @@ -1717,57 +1710,13 @@ "loadSharesFailed": "Het laden van actieve aandelen is mislukt." }, "terminal": { - "sessionExpiredNotice": "Je vorige sessie op {{host}} is na {{minutes}} minuten verlopen; de shell en alles wat erin draaide, is verdwenen. Schakel Auto-Tmux op deze host in om sessies actief te houden.", - "sessionExpiredNoticeNoMinutes": "Je vorige sessie op {{host}} is verlopen terwijl de verbinding verbroken was; de shell en alles wat daarin draaide, is verdwenen. Schakel Auto-Tmux in op deze host om sessies actief te houden.", - "enableAutoTmuxAction": "Auto-Tmux inschakelen", - "autoTmuxEnabled": "Auto-Tmux ingeschakeld voor {{host}}. Dit geldt vanaf de volgende verbinding.", - "autoTmuxEnableFailed": "Auto-Tmux inschakelen is mislukt.", "connect": "Verbinden met de host", "clear": "Duidelijk", "paste": "Plakken", - "reconnect": "Opnieuw verbinding maken", - "connectionLost": "Verbinding verbroken", "connected": "Aangesloten", "clipboardWriteFailed": "Kopiëren naar klembord mislukt. Zorg ervoor dat de pagina via HTTPS of localhost wordt aangeboden.", "clipboardReadFailed": "Het lezen van het klembord is mislukt. Zorg ervoor dat de juiste klembordrechten zijn verleend.", - "clipboardHttpWarning": "Kon het klembord niet lezen. Sommige browsers beperken de toegang tot het klembord buiten HTTPS - probeer in plaats daarvan Ctrl+V, of serveer Termix via HTTPS.", - "passwordPromptFillTitle": "Voer het opgeslagen wachtwoord in op dit veld.", - "unknownError": "Er is een onbekende fout opgetreden.", - "websocketError": "WebSocket-verbindingsfout", - "connecting": "Verbinden...", - "noHostSelected": "Geen host geselecteerd", - "reconnecting": "Opnieuw verbinding maken... ({{attempt}}/{{max}})", - "reconnected": "De verbinding is succesvol hersteld.", - "tmuxSessionCreated": "tmux-sessie aangemaakt: {{name}}", - "tmuxSessionAttached": "tmux-sessie gekoppeld: {{name}}", - "tmuxUnavailable": "Tmux is niet geïnstalleerd op de externe host, er wordt teruggevallen op de standaard shell.", - "tmuxSessionPickerTitle": "tmux-sessies", - "tmuxSessionPickerDesc": "Bestaande tmux-sessies gevonden op deze host. Selecteer er een om opnieuw verbinding te maken of een nieuwe sessie te creëren.", - "tmuxWindows": "Windows", - "tmuxWindowCount": "{{count}} venster", - "tmuxAttached": "Aangesloten klanten", - "tmuxAttachedCount": "{{count}} bevestigd", - "tmuxLastActivity": "Laatste activiteit", - "tmuxTimeJustNow": "zojuist", - "tmuxTimeMinutes": "{{count}}m geleden", - "tmuxTimeHours": "{{count}}uur geleden", - "tmuxTimeDays": "{{count}}dagen geleden", - "tmuxCreateNew": "Start een nieuwe sessie", - "tmuxCopyHint": "Pas je selectie aan en druk op Enter om naar het klembord te kopiëren.", "tmuxDetach": "Ontkoppel van de tmux-sessie", - "tmuxDetached": "Losgekoppeld van de tmux-sessie", - "searchPlaceholder": "Vinden", - "searchCaseSensitive": "Lucifersdoosje", - "searchWholeWord": "Combineer het hele woord", - "searchRegex": "Gebruik een reguliere expressie.", - "searchNoResults": "Geen resultaten", - "searchResultCount": "{{index}} van {{count}}", - "searchNext": "Volgende wedstrijd (Invoeren)", - "searchPrevious": "Vorige wedstrijd (Shift+Enter)", - "searchClose": "Sluiten (Escape)", - "maxReconnectAttemptsReached": "Maximaal aantal herverbindingspogingen bereikt", - "closeTab": "Dichtbij", - "connectionTimeout": "Verbindingstime-out", "terminalTitle": "Terminal - {{host}}", "terminalWithPath": "Terminal - {{host}}:{{path}}", "runTitle": "Bezig met uitvoeren van {{command}} - {{host}}", @@ -1790,27 +1739,14 @@ "opksshOpenBrowser": "Open de browser om te authenticeren.", "opksshWaitingForAuth": "Wachten op authenticatie in de browser...", "opksshAuthenticating": "Authenticatie wordt verwerkt...", - "opksshTimeout": "De authenticatie is mislukt vanwege een time-out. Probeer het opnieuw.", - "opksshAuthFailed": "Authenticatie mislukt. Controleer uw inloggegevens en probeer het opnieuw.", "opksshSignInWith": "Aanmelden met {{provider}}", - "vaultAuthTitle": "Aanmelden bij Vault is vereist.", - "vaultAuthDescription": "Er is een venster geopend om in te loggen bij HashiCorp Vault. Voltooi de aanmelding daar; deze verbinding wordt automatisch tot stand gebracht.", - "vaultAuthFailed": "Authenticatie voor de kluis is mislukt. Probeer het opnieuw.", - "vaultReopen": "Heropen het aanmeldvenster", "sudoPasswordPopupTitle": "Wachtwoord invoeren?", - "linkDialogTitle": "Open link", - "linkDialogOpen": "Open", - "linkDialogCopy": "Kopiëren", - "websocketAbnormalClose": "De verbinding is onverwacht verbroken. Dit kan te wijten zijn aan een probleem met de reverse proxy of de SSL-configuratie. Controleer de serverlogboeken.", "connectionLogTitle": "Verbindingslogboek", "connectionLogCopy": "Kopieer logbestanden naar het klembord.", "connectionLogEmpty": "Nog geen verbindingslogboeken", "connectionLogWaiting": "Wachten op verbindingslogboeken...", "connectionLogCopied": "Verbindingslogboeken naar het klembord gekopieerd.", "connectionLogCopyFailed": "Het kopiëren van logbestanden naar het klembord is mislukt.", - "connectionRejected": "Verbinding geweigerd door de server. Controleer uw authenticatie- en netwerkconfiguratie.", - "hostKeyRejected": "SSH-hostsleutelverificatie geweigerd. Verbinding verbroken.", - "sessionTakenOver": "De sessie is in een ander tabblad geopend. Opnieuw verbinding maken...", "split": { "splitTab": "Gesplitst tabblad", "addToSplit": "Toevoegen aan splitsen", @@ -2225,55 +2161,6 @@ "sectionApiKeys": "API-sleutels", "sectionAuditLog": "Auditlogboek", "sectionSsl": "SSL / Let's Encrypt", - "sectionTouchInput": "Aanraakinvoer", - "touchEnabled": "Aanraakinvoer inschakelen", - "touchEnabledDesc": "Schakel scrollen met één vinger in op terminals in.", - "touchMomentumEnabled": "Activeer momentum", - "touchMomentumEnabledDesc": "Scroll nog even kort verder na een veegbeweging.", - "touchAdvanced": "Geavanceerd", - "touchDragThreshold": "Sleepdrempel", - "touchMaxWheelDelta": "Maximale wieldelta", - "touchMomentumSampleWindow": "Momentum-steekproefvenster", - "touchReleaseGracePeriod": "Vrijlatingstermijn", - "touchMinimumVelocity": "Minimale snelheid", - "touchMaximumVelocity": "Maximale snelheid", - "touchMaximumDuration": "Maximale duur", - "touchMaximumTravel": "Maximale reis", - "touchDecayTime": "Vervaltijd", - "touchPixelsPerTick": "Pixels per tik", - "touchMaximumFrameInterval": "Maximale frame-interval", - "touchMaximumTicksPerFrame": "Maximaal aantal ticks per frame", - "touchResetDefaults": "Terugzetten naar standaardinstellingen", - "touchSaved": "Aanraakinvoerinstellingen opgeslagen", - "touchSaveFailed": "Het opslaan van de aanraakinvoerinstellingen is mislukt.", - "sectionImageStorage": "Afbeeldingsopslag", - "imageStorageMode": "Opslagmodus", - "imageStorageModeDesc": "Hier worden geplakte en geüploade terminalafbeeldingen opgeslagen. Automatische selectie op basis van de verbinding, lokaal en extern (SFTP) zijn vastgelegd.", - "imageStorageModeAuto": "Auto", - "imageStorageModeLocal": "Lokaal", - "imageStorageModeRemoteSftp": "Op afstand (SFTP)", - "imageStorageLocalDir": "Lokale map", - "imageStorageLocalDirDesc": "Het absolute pad in de Termix-backend. Wordt na het opslaan om veiligheidsredenen niet weergegeven.", - "imageStorageLocalDirPlaceholder": "Laat dit veld leeg om de huidige pagina te behouden.", - "imageStorageHostPath": "Hostpad", - "imageStorageHostPathDesc": "De weergave aan de hostzijde van de lokale directory die terminalagents ontvangen.", - "imageStorageTtl": "TTL (ms)", - "imageStorageMaxCount": "Maximaal aantal afbeeldingen", - "imageStorageMaxBytes": "Maximale opslagcapaciteit in bytes", - "imageStorageSaved": "Instellingen voor beeldopslag opgeslagen", - "imageStorageSaveFailed": "Het opslaan van de instellingen voor beeldopslag is mislukt.", - "imageStorageInstanceId": "Terminal sessie instantie-ID", - "imageStorageInstanceIdDesc": "Instantie-ID van een van uw verbonden terminaltabbladen, gebruikt om de zichtbaarheid te testen.", - "imageStorageInstanceIdRequired": "Voer eerst een instantie-ID van een terminalsessie in.", - "imageStorageTest": "Test", - "imageStorageTesting": "Testen...", - "imageStorageTestFailed": "Het testen van de beeldopslag is mislukt.", - "imageStorageTestConnected": "De terminalsessie is verbonden.", - "imageStorageTestNotConnected": "Er is geen verbonden terminalsessie met deze instantie-ID.", - "imageStorageTestSelectedMode": "Een upload zou het volgende gebruiken: {{mode}}", - "imageStorageSelectedModeUnavailable": "Niet beschikbaar", - "imageStorageTestLocalVisible": "De lokale map is zichtbaar vanaf de verbonden host.", - "imageStorageTestLocalNotVisible": "De lokale map is NIET zichtbaar vanaf de verbonden host.", "sectionBranding": "White Label", "brandingAppName": "Applicatienaam", "brandingAppNameDesc": "Weergegeven op het inlogscherm en in het browsertabblad. Laat dit veld leeg om \"Termix\" te gebruiken.", @@ -2354,9 +2241,6 @@ "oidcSilentLoginDefaultLockedDesc": "Deze instelling is vergrendeld door de omgevingsvariabele OIDC_SILENT_LOGIN_DEFAULT en kan hier niet worden gewijzigd.", "allowPasswordReset": "Wachtwoordherstel toestaan", "allowPasswordResetDesc": "Resetcode via Docker-logs", - "commandHistoryEnabled": "Commandogeschiedenis", - "commandHistoryEnabledDesc": "Sta het vastleggen van de opdrachtgeschiedenis toe. Indien uitgeschakeld, wordt de geschiedenis niet opgeslagen, ongeacht de instellingen per host.", - "updateCommandHistoryFailed": "Het bijwerken van de instelling voor de opdrachtgeschiedenis is mislukt.", "analyticsEnabled": "Deel anonieme gebruiksstatistieken", "analyticsEnabledDesc": "Verstuurt dagelijks anoniem een telling van gebruikers, hosts en functiegebruik om Termix te verbeteren. Er worden nooit persoonlijke gegevens of verbindingsdetails meegestuurd.", "analyticsEnabledLockedDesc": "Deze instelling is vergrendeld door de omgevingsvariabele ENABLE_TELEMETRY en kan hier niet worden gewijzigd.", @@ -2647,11 +2531,6 @@ "hostDefaultsTerminal": "Terminal", "hostDefaultsAutoTmux": "Auto-Tmux", "hostDefaultsAutoTmuxDesc": "Start de terminals van nieuwe hosts binnen tmux, zodat sessies behouden blijven bij verbroken verbindingen (tmux moet op de host geïnstalleerd zijn).", - "terminalSessionTimeout": "Terminalsessiepersistentie", - "terminalSessionTimeoutDesc": "Hoe lang een losgekoppelde terminal actief blijft op de server voordat deze wordt gesloten. Min. 1 · Max. 1440 minuten. Voor sessies die langer dan deze tijd actief moeten blijven, schakelt u Auto-Tmux in op de host.", - "terminalSessionTimeoutRange": "Voer een waarde in tussen 1 en 1440 minuten.", - "terminalSessionTimeoutSaved": "De persistentie van de terminalsessie is opgeslagen.", - "terminalSessionTimeoutSaveFailed": "Het opslaan van de persistentie van de terminalsessie is mislukt.", "minutes": "notulen", "hostDefaultsSessionLogging": "Sessieregistratie", "hostDefaultsSessionLoggingDesc": "Terminalsessies op nieuwe hosts standaard vastleggen", @@ -2708,15 +2587,6 @@ "connectToTerminal": "Verbinden met de terminal", "connectToFiles": "Verbinding maken met bestanden" }, - "history": { - "noTerminalSelected": "Geen terminal geselecteerd", - "noTerminalSelectedHint": "Open een SSH-terminaltabblad om de opdrachtgeschiedenis te bekijken.", - "searchPlaceholder": "Zoekgeschiedenis...", - "clearAll": "Alles wissen", - "noHistoryEntries": "Geen geschiedenisvermeldingen", - "trackingDisabled": "Geschiedenisregistratie is uitgeschakeld", - "trackingDisabledHint": "Schakel het in via de terminalinstellingen van de host." - }, "sshTools": { "keyRecordingTitle": "Toonopname", "recordToTerminals": "Opnemen naar terminals", @@ -3178,30 +3048,11 @@ "terminalToolbar": { "title": "Terminalwerkbalk", "showActions": "Toon de werkbalk", - "hideToolbar": "Werkbalk verbergen", - "showToolbar": "Werkbalk uitklappen", - "moveToolbar": "Verplaats de werkbalk", - "moveToolbarHint": "Sleep om de werkbalk te verplaatsen.", - "layout": "Werkbalkweergavemodus", - "layoutIcon": "Alleen pictogram", - "layoutLabeled": "Pictogrammen en labels", - "layoutExpanded": "Uitgebreid met meetgegevens", - "image": "Acties met afbeeldingen (alleen afbeeldingen)", - "upload": "Afbeelding uploaden", - "paste": "Plak afbeelding", - "detachTmux": "Ontkoppel tmux", - "detachTmuxDescription": "Ontkoppel de tmux-sessie", - "imageActionFailed": "Afbeeldingsactie mislukt", - "retry": "Opnieuw proberen", - "dismissError": "Afbeeldingsfout negeren", "cpu": "CPU", "memory": "Geheugen", "disk": "Schijf", "statsUnavailable": "Live statistieken niet beschikbaar", "tmuxDetach": "Ontkoppel de tmux-sessie", - "uploadImage": "Upload een afbeelding naar de host.", - "uploadingImage": "Bezig met uploaden...", - "pasteImage": "Plak een afbeelding vanuit het klembord (alleen afbeeldingen)", "openFiles": "Open bestanden", "openDocker": "Docker openen", "openTunnel": "Open tunnel", @@ -3285,31 +3136,6 @@ "timeHours": "{{count}}uur geleden", "timeDays": "{{count}}dagen geleden" }, - "mobileKeyboard": { - "shift": "Verschuiving", - "ctrl": "Ctrl", - "esc": "Esc", - "tab": "Tab", - "backTab": "⇥", - "arrowUp": "Pijl omhoog", - "arrowDown": "Pijl omlaag", - "arrowLeft": "Pijl naar links", - "arrowRight": "Pijl naar rechts", - "home": "Thuis", - "end": "Einde", - "pageUp": "PgUp", - "pageDown": "PgDn", - "delete": "Del", - "paste": "Plakken", - "editQuickKeys": "Sneltoetsen bewerken", - "quickKeysTitle": "Sneltoetsen", - "quickKeysDesc": "Tik op × om te verwijderen. Ondersteunt maximaal 8 tekens.", - "quickKeyPlaceholder": "bijv. sudo ", - "addQuickKey": "Toevoegen", - "removeQuickKey": "Verwijderen", - "resetDefaults": "Terugzetten naar standaardinstellingen", - "done": "Klaar" - }, "serial": { "title": "Serieel", "portLabel": "Haven", diff --git a/src/ui/locales/translated/no_NO.json b/src/ui/locales/translated/no_NO.json index c2966f1bf..c85ea4837 100644 --- a/src/ui/locales/translated/no_NO.json +++ b/src/ui/locales/translated/no_NO.json @@ -742,7 +742,6 @@ "addHost": "Legg til vert", "editHost": "Rediger vert", "cloneHost": "Klonvert", - "enableTerminal": "Aktiver terminalen", "enableTunnel": "Aktiver tunnel", "enableFileManager": "Aktiver filbehandler", "enableDocker": "Aktiver Docker", @@ -1041,8 +1040,6 @@ "enableSessionLoggingDesc": "Ta opp utdata fra terminaløkten for senere gjennomgang", "allowSessionSharing": "Tillat øktdeling", "allowSessionSharingDesc": "La direktesendte økter på denne verten deles via lenke eller med andre brukere", - "enableCommandHistory": "Kommandohistorikk", - "enableCommandHistoryDesc": "Record-kommandoer kjøres i denne terminalen for historikk og autofullføring", "localEchoLabel": "Lokalt ekko", "localEchoDesc": "Forutsi trygge utskrivbare tastetrykk lokalt på trege tilkoblinger. Passordforespørsler og kontrollinndata gjentas aldri.", "localEchoDefault": "Bruk global innstilling", @@ -1072,8 +1069,6 @@ "bindHost": "Bind vert", "enableTmuxMonitor": "Aktiver Tmux-monitor", "enableTmuxMonitorDesc": "Vis denne verten i Tmux-monitoren og legg til tmux-handlingene i sidefeltet", - "enableTerminalToolbar": "Aktiver terminalverktøylinjen", - "enableTerminalToolbarDesc": "Vis en verktøylinje for hurtig tilgang som er festet til den aktive terminalen for denne vertens verktøy og livestatistikk", "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.", @@ -1550,9 +1545,7 @@ "links": "Lenker", "navigate": "Navigere", "select": "Velge", - "toggleWith": "Veksle med", - "localTerminal": "Lokal terminal", - "localTerminalDesc": "Åpne et skall på denne datamaskinen" + "toggleWith": "Veksle med" }, "splitScreen": { "paneEmpty": "Rute {{index}} – tom", @@ -1717,57 +1710,13 @@ "loadSharesFailed": "Kunne ikke laste inn aktive delinger" }, "terminal": { - "sessionExpiredNotice": "Din forrige økt på {{host}} utløp etter {{minutes}} minutter frakobling; skallet og alt som kjører i det er borte. Aktiver Auto-Tmux på denne verten for å holde øktene aktive.", - "sessionExpiredNoticeNoMinutes": "Din forrige økt på {{host}} utløp mens den var frakoblet; skallet og alt som kjører i det er borte. Aktiver Auto-Tmux på denne verten for å holde øktene aktive.", - "enableAutoTmuxAction": "Aktiver Auto-Tmux", - "autoTmuxEnabled": "Auto-Tmux er aktivert for {{host}}. Det gjelder fra neste tilkobling.", - "autoTmuxEnableFailed": "Kunne ikke aktivere Auto-Tmux", "connect": "Koble til vert", "clear": "Klar", "paste": "Lim inn", - "reconnect": "Koble til igjen", - "connectionLost": "Forbindelsen gikk tapt", "connected": "Tilkoblet", "clipboardWriteFailed": "Kunne ikke kopiere til utklippstavlen. Sørg for at siden serveres via HTTPS eller localhost.", "clipboardReadFailed": "Kunne ikke lese fra utklippstavlen. Sørg for at utklippstavlen har tillatelser.", - "clipboardHttpWarning": "Kunne ikke lese utklippstavlen. Noen nettlesere begrenser tilgang til utklippstavlen utenfor HTTPS – prøv Ctrl+V i stedet, eller server Termix over HTTPS.", - "passwordPromptFillTitle": "Skriv inn det lagrede passordet i denne meldingen?", - "unknownError": "Ukjent feil oppstod", - "websocketError": "WebSocket-tilkoblingsfeil", - "connecting": "Kobler til...", - "noHostSelected": "Ingen vert valgt", - "reconnecting": "Kobler til på nytt... ({{attempt}}/{{max}})", - "reconnected": "Tilkoblet på nytt", - "tmuxSessionCreated": "tmux-økt opprettet: {{name}}", - "tmuxSessionAttached": "tmux-økt vedlagt: {{name}}", - "tmuxUnavailable": "tmux er ikke installert på den eksterne verten, og går tilbake til standard skall", - "tmuxSessionPickerTitle": "tmux-økter", - "tmuxSessionPickerDesc": "Eksisterende tmux-økter funnet på denne verten. Velg én for å koble til på nytt eller opprett en ny økt.", - "tmuxWindows": "Vinduer", - "tmuxWindowCount": "{{count}} vindu", - "tmuxAttached": "Tilknyttede klienter", - "tmuxAttachedCount": "{{count}} vedlagt", - "tmuxLastActivity": "Siste aktivitet", - "tmuxTimeJustNow": "akkurat nå", - "tmuxTimeMinutes": "For {{count}}minutter siden", - "tmuxTimeHours": "for {{count}}t siden", - "tmuxTimeDays": "for {{count}}dager siden", - "tmuxCreateNew": "Start ny økt", - "tmuxCopyHint": "Juster valget og trykk Enter for å kopiere til utklippstavlen", "tmuxDetach": "Koble fra tmux-økten", - "tmuxDetached": "Frakoblet fra tmux-økten", - "searchPlaceholder": "Finne", - "searchCaseSensitive": "Match-saken", - "searchWholeWord": "Finn hele ordet", - "searchRegex": "Bruk regulært uttrykk", - "searchNoResults": "Ingen resultater", - "searchResultCount": "{{index}} av {{count}}", - "searchNext": "Neste kamp (Enter)", - "searchPrevious": "Forrige treff (Shift+Enter)", - "searchClose": "Lukk (Escape)", - "maxReconnectAttemptsReached": "Maksimalt antall forsøk på å koble til igjen er nådd", - "closeTab": "Lukke", - "connectionTimeout": "Tidsavbrudd for tilkobling", "terminalTitle": "Terminal - {{host}}", "terminalWithPath": "Terminal - {{host}}:{{path}}", "runTitle": "Kjører {{command}} - {{host}}", @@ -1790,27 +1739,14 @@ "opksshOpenBrowser": "Åpne nettleseren for å autentisere", "opksshWaitingForAuth": "Venter på autentisering i nettleseren...", "opksshAuthenticating": "Behandler autentisering ...", - "opksshTimeout": "Autentiseringen ble tidsavbrutt. Prøv på nytt.", - "opksshAuthFailed": "Autentiseringen mislyktes. Sjekk legitimasjonen din og prøv på nytt.", "opksshSignInWith": "Logg inn med {{provider}}", - "vaultAuthTitle": "Pålogging til arkivet kreves", - "vaultAuthDescription": "Et vindu har åpnet seg for å logge på HashiCorp Vault. Fullfør påloggingen der; denne tilkoblingen vil fortsette automatisk.", - "vaultAuthFailed": "Hvelv-autentisering mislyktes. Prøv på nytt.", - "vaultReopen": "Åpne påloggingsvinduet på nytt", "sudoPasswordPopupTitle": "Sette inn passord?", - "linkDialogTitle": "Åpne lenke", - "linkDialogOpen": "Åpne", - "linkDialogCopy": "Kopiere", - "websocketAbnormalClose": "Tilkoblingen ble avsluttet uventet. Dette kan skyldes et problem med omvendt proxy eller SSL-konfigurasjon. Sjekk serverloggene.", "connectionLogTitle": "Tilkoblingslogg", "connectionLogCopy": "Kopier logger til utklippstavlen", "connectionLogEmpty": "Ingen tilkoblingslogger ennå", "connectionLogWaiting": "Venter på tilkoblingslogger ...", "connectionLogCopied": "Tilkoblingslogger kopiert til utklippstavlen", "connectionLogCopyFailed": "Kunne ikke kopiere logger til utklippstavlen", - "connectionRejected": "Tilkoblingen ble avvist av serveren. Sjekk autentiseringen og nettverkskonfigurasjonen din.", - "hostKeyRejected": "Verifisering av SSH-vertsnøkkel avvist. Tilkoblingen ble avbrutt.", - "sessionTakenOver": "Økten ble åpnet i en annen fane. Kobler til igjen...", "split": { "splitTab": "Del fane", "addToSplit": "Legg til i deling", @@ -2225,55 +2161,6 @@ "sectionApiKeys": "API-nøkler", "sectionAuditLog": "Revisjonslogg", "sectionSsl": "SSL / La oss kryptere", - "sectionTouchInput": "Berøringsinndata", - "touchEnabled": "Aktiver berøringsinndata", - "touchEnabledDesc": "Tillat énfinger-berøringsrulling i terminaler", - "touchMomentumEnabled": "Aktiver momentum", - "touchMomentumEnabledDesc": "Fortsett å bla kort etter et sveip", - "touchAdvanced": "Avansert", - "touchDragThreshold": "Dragterskel", - "touchMaxWheelDelta": "Maksimal hjuldelta", - "touchMomentumSampleWindow": "Momentum-eksempelvindu", - "touchReleaseGracePeriod": "Frigivelsesfrist", - "touchMinimumVelocity": "Minimumshastighet", - "touchMaximumVelocity": "Maksimal hastighet", - "touchMaximumDuration": "Maksimal varighet", - "touchMaximumTravel": "Maksimal reise", - "touchDecayTime": "Forfallstid", - "touchPixelsPerTick": "Piksler per tikk", - "touchMaximumFrameInterval": "Maksimalt bildeintervall", - "touchMaximumTicksPerFrame": "Maksimalt antall ticks per ramme", - "touchResetDefaults": "Tilbakestill til standardinnstillinger", - "touchSaved": "Innstillinger for berøringsinndata lagret", - "touchSaveFailed": "Kunne ikke lagre innstillingene for berøringsinndata", - "sectionImageStorage": "Bildelagring", - "imageStorageMode": "Lagringsmodus", - "imageStorageModeDesc": "Der innlimte og opplastede terminalbilder skrives. Automatiske valg etter tilkobling, lokal og ekstern (SFTP) er fikset.", - "imageStorageModeAuto": "Bil", - "imageStorageModeLocal": "Lokalt", - "imageStorageModeRemoteSftp": "Fjernstyrt (SFTP)", - "imageStorageLocalDir": "Lokal katalog", - "imageStorageLocalDirDesc": "Absolutt sti på Termix-backend. Vises ikke etter lagring av sikkerhetshensyn.", - "imageStorageLocalDirPlaceholder": "La stå tomt for å holde deg oppdatert", - "imageStorageHostPath": "Vertssti", - "imageStorageHostPathDesc": "Vertsvisningen av den lokale katalogen som terminalagenter mottar.", - "imageStorageTtl": "TTL (ms)", - "imageStorageMaxCount": "Maksimalt antall bilder", - "imageStorageMaxBytes": "Maksimalt antall lagringsbyte", - "imageStorageSaved": "Innstillinger for bildelagring er lagret", - "imageStorageSaveFailed": "Kunne ikke lagre innstillingene for bildelagring", - "imageStorageInstanceId": "Terminaløktforekomst-ID", - "imageStorageInstanceIdDesc": "Forekomst-ID for en av dine tilkoblede terminalfaner, brukt til å teste synlighet.", - "imageStorageInstanceIdRequired": "Skriv inn en terminaløktinstans-ID først", - "imageStorageTest": "Test", - "imageStorageTesting": "Testing...", - "imageStorageTestFailed": "Kunne ikke teste bildelagring", - "imageStorageTestConnected": "Terminaløkten er tilkoblet.", - "imageStorageTestNotConnected": "Ingen tilkoblet terminaløkt med denne forekomst-ID-en.", - "imageStorageTestSelectedMode": "En opplasting ville bruke: {{mode}}", - "imageStorageSelectedModeUnavailable": "Utilgjengelig", - "imageStorageTestLocalVisible": "Den lokale katalogen er synlig fra den tilkoblede verten.", - "imageStorageTestLocalNotVisible": "Den lokale katalogen er IKKE synlig fra den tilkoblede verten.", "sectionBranding": "Hvit etikett", "brandingAppName": "Programnavn", "brandingAppNameDesc": "Vises på innloggingsskjermen og i nettleserfanen. La stå tomt for å bruke «Termix».", @@ -2354,9 +2241,6 @@ "oidcSilentLoginDefaultLockedDesc": "Denne innstillingen er låst av miljøvariabelen OIDC_SILENT_LOGIN_DEFAULT og kan ikke endres her.", "allowPasswordReset": "Tillat tilbakestilling av passord", "allowPasswordResetDesc": "Tilbakestill kode via Docker-logger", - "commandHistoryEnabled": "Kommandohistorikk", - "commandHistoryEnabledDesc": "Tillat opptak av kommandohistorikk. Når den er deaktivert, lagres ikke historikken uavhengig av innstillinger per vert.", - "updateCommandHistoryFailed": "Kunne ikke oppdatere innstillingen for kommandohistorikk", "analyticsEnabled": "Del anonym bruksstatistikk", "analyticsEnabledDesc": "Sender en anonym daglig telling av brukere, verter og funksjonsbruk for å forbedre Termix. Ingen personopplysninger eller tilkoblingsdetaljer inkluderes.", "analyticsEnabledLockedDesc": "Denne innstillingen er låst av miljøvariabelen ENABLE_TELEMETRY og kan ikke endres her.", @@ -2647,11 +2531,6 @@ "hostDefaultsTerminal": "Terminal", "hostDefaultsAutoTmux": "Auto-Tmux", "hostDefaultsAutoTmuxDesc": "Kjør nye verters terminaler inne i tmux slik at økter overlever frakoblinger (trenger tmux på verten)", - "terminalSessionTimeout": "Terminaløktpersistens", - "terminalSessionTimeoutDesc": "Hvor lenge en frakoblet terminal holdes aktiv på serveren før den lukkes. Min 1 · Maks 1440 minutter. For økter som må overleve dette, aktiver Auto-Tmux på verten.", - "terminalSessionTimeoutRange": "Skriv inn en verdi mellom 1 og 1440 minutter", - "terminalSessionTimeoutSaved": "Terminaløktens varighet lagret", - "terminalSessionTimeoutSaveFailed": "Kunne ikke lagre terminaløktens varighet", "minutes": "minutter", "hostDefaultsSessionLogging": "Øktlogging", "hostDefaultsSessionLoggingDesc": "Spill inn terminaløkter på nye verter som standard", @@ -2708,15 +2587,6 @@ "connectToTerminal": "Koble til terminalen", "connectToFiles": "Koble til filer" }, - "history": { - "noTerminalSelected": "Ingen terminal valgt", - "noTerminalSelectedHint": "Åpne en SSH-terminalfane for å se kommandohistorikken", - "searchPlaceholder": "Søkelogg...", - "clearAll": "Fjern alt", - "noHistoryEntries": "Ingen historikkoppføringer", - "trackingDisabled": "Historikksporing er deaktivert", - "trackingDisabledHint": "Aktiver det i vertens terminalinnstillinger." - }, "sshTools": { "keyRecordingTitle": "Nøkkelopptak", "recordToTerminals": "Opptak til terminaler", @@ -3178,30 +3048,11 @@ "terminalToolbar": { "title": "Terminalverktøylinje", "showActions": "Vis verktøylinje", - "hideToolbar": "Skjul verktøylinjen", - "showToolbar": "Utvid verktøylinjen", - "moveToolbar": "Flytt verktøylinjen", - "moveToolbarHint": "Dra for å flytte verktøylinjen", - "layout": "Visningsmodus for verktøylinjen", - "layoutIcon": "Kun ikon", - "layoutLabeled": "Ikoner og etiketter", - "layoutExpanded": "Utvidet med målinger", - "image": "Bildehandlinger (kun bilder)", - "upload": "Last opp bilde", - "paste": "Lim inn bilde", - "detachTmux": "Koble fra tmux", - "detachTmuxDescription": "Koble fra tmux-økten", - "imageActionFailed": "Bildehandlingen mislyktes", - "retry": "Prøv på nytt", - "dismissError": "Avvis bildefeil", "cpu": "CPU", "memory": "Hukommelse", "disk": "Disk", "statsUnavailable": "Livestatistikk er ikke tilgjengelig", "tmuxDetach": "Koble fra tmux-økten", - "uploadImage": "Last opp et bilde til verten", - "uploadingImage": "Laster opp...", - "pasteImage": "Lim inn bilde fra utklippstavlen (kun bilder)", "openFiles": "Åpne filer", "openDocker": "Åpne Docker", "openTunnel": "Åpen tunnel", @@ -3285,31 +3136,6 @@ "timeHours": "for {{count}}t siden", "timeDays": "for {{count}}dager siden" }, - "mobileKeyboard": { - "shift": "Skifte", - "ctrl": "Ctrl", - "esc": "Esc", - "tab": "Fane", - "backTab": "⇥", - "arrowUp": "Pil opp", - "arrowDown": "Pil ned", - "arrowLeft": "Pil venstre", - "arrowRight": "Pil høyre", - "home": "Hjem", - "end": "Slutt", - "pageUp": "PgUp", - "pageDown": "PgDn", - "delete": "Del", - "paste": "Lim inn", - "editQuickKeys": "Rediger hurtigtaster", - "quickKeysTitle": "Hurtigtaster", - "quickKeysDesc": "Trykk på × for å fjerne. Støtter opptil 8 tegn.", - "quickKeyPlaceholder": "f.eks. sudo ", - "addQuickKey": "Legge til", - "removeQuickKey": "Fjerne", - "resetDefaults": "Tilbakestill til standardinnstillinger", - "done": "Ferdig" - }, "serial": { "title": "Seriell", "portLabel": "Havn", diff --git a/src/ui/locales/translated/pl_PL.json b/src/ui/locales/translated/pl_PL.json index 4fe76717e..fb1c9431a 100644 --- a/src/ui/locales/translated/pl_PL.json +++ b/src/ui/locales/translated/pl_PL.json @@ -742,7 +742,6 @@ "addHost": "Dodaj hosta", "editHost": "Edytuj hosta", "cloneHost": "Klonuj hosta", - "enableTerminal": "Włącz terminal", "enableTunnel": "Włącz tunel", "enableFileManager": "Włącz menedżer plików", "enableDocker": "Włącz Docker", @@ -1041,8 +1040,6 @@ "enableSessionLoggingDesc": "Zapisuj wyjście sesji terminala do późniejszego przejrzenia", "allowSessionSharing": "Zezwól na udostępnianie sesji", "allowSessionSharingDesc": "Zezwól na udostępnianie sesji na żywo na tym hoście przez link lub innym użytkownikom", - "enableCommandHistory": "Historia poleceń", - "enableCommandHistoryDesc": "Zapisuj polecenia uruchamiane w tym terminalu na potrzeby historii i autouzupełniania", "localEchoLabel": "Lokalne echo", "localEchoDesc": "Przewiduj lokalnie bezpieczne, drukowalne znaki na wolnych łączach. Monity o hasło i znaki sterujące nigdy nie są powtarzane.", "localEchoDefault": "Użyj ustawienia globalnego", @@ -1072,8 +1069,6 @@ "bindHost": "Host nasłuchu", "enableTmuxMonitor": "Włącz monitor Tmux", "enableTmuxMonitorDesc": "Pokaż tego hosta w monitorze Tmux i dodaj jego akcje tmux do paska bocznego", - "enableTerminalToolbar": "Włącz pasek narzędzi terminala", - "enableTerminalToolbarDesc": "Pokaż pasek narzędzi szybkiego dostępu zadokowany do aktywnego terminala, umożliwiający dostęp do narzędzi i statystyk na żywo tego hosta", "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.", @@ -1550,9 +1545,7 @@ "links": "Linki", "navigate": "Nawigacja", "select": "Wybór", - "toggleWith": "Przełącz z", - "localTerminal": "Terminal lokalny", - "localTerminalDesc": "Otwórz powłokę na tym komputerze" + "toggleWith": "Przełącz z" }, "splitScreen": { "paneEmpty": "Panel {{index}} - pusty", @@ -1717,57 +1710,13 @@ "loadSharesFailed": "Nie udało się załadować aktywnych udostępnień" }, "terminal": { - "sessionExpiredNotice": "Twoja poprzednia sesja z {{host}} wygasła po {{minutes}} minutach odłączenia. Powłoka i wszystko, co w niej działa, zniknęło. Włącz Auto-Tmux na tym hoście, aby sesje pozostały aktywne.", - "sessionExpiredNoticeNoMinutes": "Twoja poprzednia sesja na {{host}} wygasła podczas rozłączenia; powłoka i wszystko, co w niej działa, zniknęło. Włącz Auto-Tmux na tym hoście, aby sesje pozostały aktywne.", - "enableAutoTmuxAction": "Włącz Auto-Tmux", - "autoTmuxEnabled": "Auto-Tmux włączony dla {{host}}. Ma zastosowanie od następnego połączenia.", - "autoTmuxEnableFailed": "Nie udało się włączyć funkcji Auto-Tmux", "connect": "Połącz z hostem", "clear": "Wyczyść", "paste": "Wklej", - "reconnect": "Połącz ponownie", - "connectionLost": "Utracono połączenie", "connected": "Połączono", "clipboardWriteFailed": "Nie udało się skopiować do schowka. Upewnij się, że strona jest serwowana przez HTTPS lub localhost.", "clipboardReadFailed": "Nie udało się odczytać danych ze schowka. Upewnij się, że uprawnienia do schowka zostały przyznane.", - "clipboardHttpWarning": "Nie udało się odczytać schowka. Niektóre przeglądarki ograniczają dostęp do schowka poza HTTPS. Użyj Ctrl+V albo udostępniaj Termix przez HTTPS.", - "passwordPromptFillTitle": "Czy wpisać zapisane hasło w tym monicie?", - "unknownError": "Wystąpił nieznany błąd", - "websocketError": "Błąd połączenia WebSocket", - "connecting": "Łączenie...", - "noHostSelected": "Nie wybrano hosta", - "reconnecting": "Ponowne łączenie... ({{attempt}}/{{max}})", - "reconnected": "Połączono ponownie pomyślnie", - "tmuxSessionCreated": "Utworzono sesję tmux: {{name}}", - "tmuxSessionAttached": "Dołączono do sesji tmux: {{name}}", - "tmuxUnavailable": "tmux nie jest zainstalowany na zdalnym hoście, następuje powrót do standardowej powłoki", - "tmuxSessionPickerTitle": "Sesje tmux", - "tmuxSessionPickerDesc": "Znaleziono istniejące sesje tmux na tym hoście. Wybierz jedną, aby ponownie dołączyć lub utworzyć nową sesję.", - "tmuxWindows": "Okna", - "tmuxWindowCount": "{{count}} okno", - "tmuxAttached": "Dołączeni klienci", - "tmuxAttachedCount": "{{count}} dołączonych", - "tmuxLastActivity": "Ostatnia aktywność", - "tmuxTimeJustNow": "przed chwilą", - "tmuxTimeMinutes": "{{count}} min temu", - "tmuxTimeHours": "{{count}} godz. temu", - "tmuxTimeDays": "{{count}} dni temu", - "tmuxCreateNew": "Rozpocznij nową sesję", - "tmuxCopyHint": "Dostosuj zaznaczenie i naciśnij Enter, aby skopiować do schowka", "tmuxDetach": "Odłącz od sesji tmux", - "tmuxDetached": "Odłączono od sesji tmux", - "searchPlaceholder": "Znajdź", - "searchCaseSensitive": "Uwzględnij wielkość liter", - "searchWholeWord": "Dopasuj całe słowo", - "searchRegex": "Użyj wyrażenia regularnego", - "searchNoResults": "Brak wyników", - "searchResultCount": "{{index}} z {{count}}", - "searchNext": "Następne dopasowanie (Enter)", - "searchPrevious": "Poprzednie dopasowanie (Shift+Enter)", - "searchClose": "Zamknij (Esc)", - "maxReconnectAttemptsReached": "Osiągnięto maksymalną liczbę prób ponownego połączenia", - "closeTab": "Zamknij", - "connectionTimeout": "Przekroczono limit czasu połączenia", "terminalTitle": "Terminal - {{host}}", "terminalWithPath": "Terminal - {{host}}:{{path}}", "runTitle": "Wykonywanie {{command}} - {{host}}", @@ -1790,27 +1739,14 @@ "opksshOpenBrowser": "Otwórz przeglądarkę, aby uwierzytelnić", "opksshWaitingForAuth": "Oczekiwanie na uwierzytelnienie w przeglądarce...", "opksshAuthenticating": "Przetwarzanie uwierzytelniania...", - "opksshTimeout": "Upłynął limit czasu uwierzytelniania. Spróbuj ponownie.", - "opksshAuthFailed": "Uwierzytelnienie nie powiodło się. Sprawdź poświadczenia i spróbuj ponownie.", "opksshSignInWith": "Zaloguj się za pomocą {{provider}}", - "vaultAuthTitle": "Wymagane logowanie do Vault", - "vaultAuthDescription": "Otworzyło się okno logowania do HashiCorp Vault. Dokończ logowanie, a połączenie zostanie nawiązane automatycznie.", - "vaultAuthFailed": "Uwierzytelnienie w Vault nie powiodło się. Spróbuj ponownie.", - "vaultReopen": "Otwórz ponownie okno logowania", "sudoPasswordPopupTitle": "Wprowadzić hasło?", - "linkDialogTitle": "Otwórz link", - "linkDialogOpen": "Otwórz", - "linkDialogCopy": "Kopiuj", - "websocketAbnormalClose": "Połączenie zostało nieoczekiwanie zamknięte. Przyczyną może być reverse proxy lub problem z konfiguracją SSL. Sprawdź logi serwera.", "connectionLogTitle": "Log połączenia", "connectionLogCopy": "Kopiuj logi do schowka", "connectionLogEmpty": "Brak logów połączenia", "connectionLogWaiting": "Oczekiwanie na logi połączenia...", "connectionLogCopied": "Skopiowano logi połączenia do schowka", "connectionLogCopyFailed": "Nie udało się skopiować logów do schowka", - "connectionRejected": "Połączenie odrzucone przez serwer. Sprawdź uwierzytelnianie i konfigurację sieci.", - "hostKeyRejected": "Weryfikacja klucza hosta SSH odrzucona. Połączenie anulowane.", - "sessionTakenOver": "Sesja została otwarta w innej karcie. Ponowne łączenie...", "split": { "splitTab": "Podziel kartę", "addToSplit": "Dodaj do podziału", @@ -2225,55 +2161,6 @@ "sectionApiKeys": "Klucze API", "sectionAuditLog": "Dziennik audytu", "sectionSsl": "SSL / Let's Encrypt", - "sectionTouchInput": "Wprowadzanie dotykowe", - "touchEnabled": "Włącz wprowadzanie dotykowe", - "touchEnabledDesc": "Zezwól na przewijanie jednym palcem w terminalach", - "touchMomentumEnabled": "Włącz Momentum", - "touchMomentumEnabledDesc": "Kontynuuj przewijanie przez chwilę po przesunięciu", - "touchAdvanced": "Zaawansowane", - "touchDragThreshold": "Próg przeciągania", - "touchMaxWheelDelta": "Maksymalna delta koła", - "touchMomentumSampleWindow": "Okno próbki pędu", - "touchReleaseGracePeriod": "Okres karencji po puszczeniu", - "touchMinimumVelocity": "Minimalna prędkość", - "touchMaximumVelocity": "Maksymalna prędkość", - "touchMaximumDuration": "Maksymalny czas trwania", - "touchMaximumTravel": "Maksymalny dystans", - "touchDecayTime": "Czas wygaszania", - "touchPixelsPerTick": "Pikseli na tick", - "touchMaximumFrameInterval": "Maksymalny interwał klatek", - "touchMaximumTicksPerFrame": "Maksymalna liczba tików na klatkę", - "touchResetDefaults": "Przywróć ustawienia domyślne", - "touchSaved": "Zapisano ustawienia wprowadzania dotykowego", - "touchSaveFailed": "Nie udało się zapisać ustawień wprowadzania dotykowego", - "sectionImageStorage": "Przechowywanie obrazów", - "imageStorageMode": "Tryb przechowywania", - "imageStorageModeDesc": "Gdzie zapisywane są wklejone i przesłane obrazy z terminala. Auto wybiera według połączenia; Lokalny i Zdalny (SFTP) są stałe.", - "imageStorageModeAuto": "Auto", - "imageStorageModeLocal": "Lokalny", - "imageStorageModeRemoteSftp": "Zdalny (SFTP)", - "imageStorageLocalDir": "Katalog lokalny", - "imageStorageLocalDirDesc": "Ścieżka bezwzględna na serwerze Termix. Ze względów bezpieczeństwa nie jest pokazywana po zapisaniu.", - "imageStorageLocalDirPlaceholder": "Pozostaw puste, aby zachować bieżącą wartość", - "imageStorageHostPath": "Ścieżka hosta", - "imageStorageHostPathDesc": "Widok katalogu lokalnego od strony hosta, który otrzymują agenci terminalowi.", - "imageStorageTtl": "Czas życia (ms)", - "imageStorageMaxCount": "Maksymalna liczba obrazów", - "imageStorageMaxBytes": "Maksymalny rozmiar magazynu (bajty)", - "imageStorageSaved": "Zapisano ustawienia przechowywania obrazów", - "imageStorageSaveFailed": "Nie udało się zapisać ustawień przechowywania obrazów", - "imageStorageInstanceId": "Identyfikator instancji sesji terminalowej", - "imageStorageInstanceIdDesc": "Identyfikator instancji jednej z podłączonych kart terminala, używany do testowania widoczności.", - "imageStorageInstanceIdRequired": "Najpierw wprowadź identyfikator instancji sesji terminala", - "imageStorageTest": "Test", - "imageStorageTesting": "Testowanie...", - "imageStorageTestFailed": "Nie udało się przetestować magazynu obrazów", - "imageStorageTestConnected": "Sesja terminalowa jest połączona.", - "imageStorageTestNotConnected": "Brak połączonej sesji terminala z tym identyfikatorem instancji.", - "imageStorageTestSelectedMode": "Przesyłanie użyłoby: {{mode}}", - "imageStorageSelectedModeUnavailable": "Niedostępny", - "imageStorageTestLocalVisible": "Katalog lokalny jest widoczny z podłączonego hosta.", - "imageStorageTestLocalNotVisible": "Katalog lokalny NIE jest widoczny z podłączonego hosta.", "sectionBranding": "Biała etykieta", "brandingAppName": "Nazwa aplikacji", "brandingAppNameDesc": "Wyświetlane na ekranie logowania i karcie przeglądarki. Pozostaw puste, aby używać „Termix”.", @@ -2354,9 +2241,6 @@ "oidcSilentLoginDefaultLockedDesc": "To ustawienie jest zablokowane przez zmienną środowiskową OIDC_SILENT_LOGIN_DEFAULT i nie można go tutaj zmienić.", "allowPasswordReset": "Zezwól na resetowanie hasła", "allowPasswordResetDesc": "Kod resetowania przez logi Dockera", - "commandHistoryEnabled": "Historia poleceń", - "commandHistoryEnabledDesc": "Zezwól na rejestrowanie historii poleceń. Po wyłączeniu historia nie jest zapisywana, niezależnie od ustawień poszczególnych hostów.", - "updateCommandHistoryFailed": "Nie udało się zaktualizować ustawień historii poleceń", "analyticsEnabled": "Udostępnij anonimowe statystyki użytkowania", "analyticsEnabledDesc": "Wysyła anonimowe, dzienne zestawienie liczby użytkowników, hostów i wykorzystania funkcji, aby pomóc w ulepszaniu Termixa. Nigdy nie zawiera danych osobowych ani szczegółów połączeń.", "analyticsEnabledLockedDesc": "To ustawienie jest zablokowane przez zmienną środowiskową ENABLE_TELEMETRY i nie można go tutaj zmienić.", @@ -2647,11 +2531,6 @@ "hostDefaultsTerminal": "Terminal", "hostDefaultsAutoTmux": "Auto-Tmux", "hostDefaultsAutoTmuxDesc": "Uruchom nowe terminale hostów w tmux, aby sesje przetrwały rozłączenia (wymaga tmux na hoście)", - "terminalSessionTimeout": "Trwałość sesji terminalowej", - "terminalSessionTimeoutDesc": "Jak długo odłączony terminal jest utrzymywany aktywny na serwerze przed jego zamknięciem. Min. 1 · Maks. 1440 minut. W przypadku sesji, które muszą przetrwać ten czas, włącz funkcję Auto-Tmux na hoście.", - "terminalSessionTimeoutRange": "Wprowadź wartość od 1 do 1440 minut", - "terminalSessionTimeoutSaved": "Zapisano trwałość sesji terminalowej", - "terminalSessionTimeoutSaveFailed": "Nie udało się zapisać trwałości sesji terminala", "minutes": "protokół", "hostDefaultsSessionLogging": "Rejestrowanie sesji", "hostDefaultsSessionLoggingDesc": "Domyślnie rejestruj sesje terminalowe na nowych hostach", @@ -2708,15 +2587,6 @@ "connectToTerminal": "Połącz z terminalem", "connectToFiles": "Połącz z plikami" }, - "history": { - "noTerminalSelected": "Nie wybrano terminala", - "noTerminalSelectedHint": "Otwórz kartę terminala SSH, aby wyświetlić historię poleceń", - "searchPlaceholder": "Przeszukaj historię...", - "clearAll": "Wyczyść wszystko", - "noHistoryEntries": "Brak wpisów w historii", - "trackingDisabled": "Śledzenie historii jest wyłączone", - "trackingDisabledHint": "Włącz tę opcję w ustawieniach terminala hosta." - }, "sshTools": { "keyRecordingTitle": "Nagrywanie klawiszy", "recordToTerminals": "Nagrywaj do terminali", @@ -3178,30 +3048,11 @@ "terminalToolbar": { "title": "Pasek narzędzi terminala", "showActions": "Pokaż pasek narzędzi", - "hideToolbar": "Ukryj pasek narzędzi", - "showToolbar": "Rozwiń pasek narzędzi", - "moveToolbar": "Przenieś pasek narzędzi", - "moveToolbarHint": "Przeciągnij, aby zmienić położenie paska narzędzi", - "layout": "Tryb wyświetlania paska narzędzi", - "layoutIcon": "Tylko ikona", - "layoutLabeled": "Ikony i etykiety", - "layoutExpanded": "Rozszerzone o metryki", - "image": "Akcje obrazu (tylko obrazy)", - "upload": "Prześlij obraz", - "paste": "Wklej obraz", - "detachTmux": "Odłącz tmux", - "detachTmuxDescription": "Odłącz sesję tmux", - "imageActionFailed": "Akcja obrazu nie powiodła się", - "retry": "Ponów", - "dismissError": "Odrzuć błąd obrazu", "cpu": "Procesor", "memory": "Pamięć", "disk": "Dysk", "statsUnavailable": "Statystyki na żywo niedostępne", "tmuxDetach": "Odłącz sesję tmux", - "uploadImage": "Prześlij obraz do hosta", - "uploadingImage": "Przesyłanie...", - "pasteImage": "Wklej obraz ze schowka (tylko obrazy)", "openFiles": "Otwórz pliki", "openDocker": "Otwórz Dockera", "openTunnel": "Otwórz tunel", @@ -3285,31 +3136,6 @@ "timeHours": "{{count}} godz. temu", "timeDays": "{{count}} dni temu" }, - "mobileKeyboard": { - "shift": "Shift", - "ctrl": "Ctrl", - "esc": "Esc", - "tab": "Tab", - "backTab": "⇥", - "arrowUp": "Strzałka w górę", - "arrowDown": "Strzałka w dół", - "arrowLeft": "Strzałka w lewo", - "arrowRight": "Strzałka w prawo", - "home": "Home", - "end": "End", - "pageUp": "PgUp", - "pageDown": "PgDn", - "delete": "Del", - "paste": "Wklej", - "editQuickKeys": "Edytuj szybkie klawisze", - "quickKeysTitle": "Szybkie klawisze", - "quickKeysDesc": "Dotknij ×, aby usunąć. Maksymalnie 8 znaków.", - "quickKeyPlaceholder": "np. sudo ", - "addQuickKey": "Dodaj", - "removeQuickKey": "Usuń", - "resetDefaults": "Przywróć ustawienia domyślne", - "done": "Gotowe" - }, "serial": { "title": "Port szeregowy", "portLabel": "Port", diff --git a/src/ui/locales/translated/pt_BR.json b/src/ui/locales/translated/pt_BR.json index 395a4be9d..9fab5b79b 100644 --- a/src/ui/locales/translated/pt_BR.json +++ b/src/ui/locales/translated/pt_BR.json @@ -742,7 +742,6 @@ "addHost": "Adicionar host", "editHost": "Editar Host", "cloneHost": "Host de clonagem", - "enableTerminal": "Habilitar terminal", "enableTunnel": "Ativar túnel", "enableFileManager": "Ativar o Gerenciador de Arquivos", "enableDocker": "Habilitar Docker", @@ -1041,8 +1040,6 @@ "enableSessionLoggingDesc": "Registre a saída da sessão do terminal para revisão posterior.", "allowSessionSharing": "Permitir compartilhamento de sessão", "allowSessionSharingDesc": "Permitir que as sessões ao vivo neste servidor sejam compartilhadas por meio de link ou com outros usuários.", - "enableCommandHistory": "Histórico de comandos", - "enableCommandHistoryDesc": "Registre os comandos executados neste terminal para histórico e preenchimento automático.", "localEchoLabel": "Eco Local", "localEchoDesc": "Preveja sequências de teclas seguras e imprimíveis localmente em conexões lentas. Os prompts de senha e as entradas de controle nunca são repetidos.", "localEchoDefault": "Usar configuração global", @@ -1072,8 +1069,6 @@ "bindHost": "Vincular host", "enableTmuxMonitor": "Ativar monitor Tmux", "enableTmuxMonitorDesc": "Exiba este host no Monitor Tmux e adicione suas ações tmux à barra lateral.", - "enableTerminalToolbar": "Ativar a barra de ferramentas do terminal", - "enableTerminalToolbarDesc": "Exibir uma barra de ferramentas de acesso rápido acoplada ao terminal ativo com as ferramentas e estatísticas em tempo real deste host.", "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.", @@ -1550,9 +1545,7 @@ "links": "Links", "navigate": "Navegar", "select": "Selecione", - "toggleWith": "Alternar com", - "localTerminal": "Terminal local", - "localTerminalDesc": "Abra um terminal neste computador." + "toggleWith": "Alternar com" }, "splitScreen": { "paneEmpty": "Painel {{index}} - vazio", @@ -1717,57 +1710,13 @@ "loadSharesFailed": "Falha ao carregar compartilhamentos ativos" }, "terminal": { - "sessionExpiredNotice": "Sua sessão anterior em {{host}} expirou após {{minutes}} minutos desconectado; o shell e tudo o que estava sendo executado nele foram perdidos. Habilite o Auto-Tmux neste host para manter as sessões ativas.", - "sessionExpiredNoticeNoMinutes": "Sua sessão anterior em {{host}} expirou enquanto estava desconectada; o shell e tudo o que estava sendo executado nele foram perdidos. Habilite o Auto-Tmux neste host para manter as sessões ativas.", - "enableAutoTmuxAction": "Ativar Auto-Tmux", - "autoTmuxEnabled": "Auto-Tmux ativado para {{host}}. Aplica-se a partir da próxima conexão.", - "autoTmuxEnableFailed": "Falha ao ativar o Auto-Tmux", "connect": "Conectar ao host", "clear": "Claro", "paste": "Colar", - "reconnect": "Reconectar", - "connectionLost": "Conexão perdida", "connected": "Conectado", "clipboardWriteFailed": "Falha ao copiar para a área de transferência. Certifique-se de que a página esteja sendo servida via HTTPS ou localhost.", "clipboardReadFailed": "Não foi possível ler o conteúdo da área de transferência. Certifique-se de que as permissões de acesso à área de transferência estejam concedidas.", - "clipboardHttpWarning": "Não foi possível ler a área de transferência. Alguns navegadores restringem o acesso à área de transferência fora de HTTPS - tente Ctrl+V ou utilize HTTPS para acessar o Termix.", - "passwordPromptFillTitle": "Digite a senha salva neste campo?", - "unknownError": "Ocorreu um erro desconhecido.", - "websocketError": "Erro de conexão WebSocket", - "connecting": "Conectando...", - "noHostSelected": "Nenhum host selecionado", - "reconnecting": "Reconectando... ({{attempt}}/{{max}})", - "reconnected": "Reconectado com sucesso", - "tmuxSessionCreated": "Sessão tmux criada: {{name}}", - "tmuxSessionAttached": "Sessão tmux anexada: {{name}}", - "tmuxUnavailable": "O tmux não está instalado no host remoto, utilizando o shell padrão como alternativa.", - "tmuxSessionPickerTitle": "Sessões tmux", - "tmuxSessionPickerDesc": "Foram encontradas sessões tmux existentes neste host. Selecione uma para reconectar ou crie uma nova sessão.", - "tmuxWindows": "Windows", - "tmuxWindowCount": "{{count}} janela", - "tmuxAttached": "Clientes vinculados", - "tmuxAttachedCount": "{{count}} anexado", - "tmuxLastActivity": "Última atividade", - "tmuxTimeJustNow": "agora mesmo", - "tmuxTimeMinutes": "{{count}}m atrás", - "tmuxTimeHours": "{{count}}h atrás", - "tmuxTimeDays": "{{count}}há alguns dias", - "tmuxCreateNew": "Iniciar nova sessão", - "tmuxCopyHint": "Ajuste a seleção e pressione Enter para copiar para a área de transferência.", "tmuxDetach": "Desconectar da sessão tmux", - "tmuxDetached": "Sessão desconectada do tmux", - "searchPlaceholder": "Encontrar", - "searchCaseSensitive": "Caixa de fósforos", - "searchWholeWord": "Combine a palavra inteira", - "searchRegex": "Utilizar expressão regular", - "searchNoResults": "Nenhum resultado", - "searchResultCount": "{{index}} de {{count}}", - "searchNext": "Próxima partida (Entrar)", - "searchPrevious": "Partida anterior (Shift+Enter)", - "searchClose": "Fechar (Esc)", - "maxReconnectAttemptsReached": "Número máximo de tentativas de reconexão atingido", - "closeTab": "Fechar", - "connectionTimeout": "Tempo limite de conexão", "terminalTitle": "Terminal - {{host}}", "terminalWithPath": "Terminal - {{host}}:{{path}}", "runTitle": "Executando {{command}} - {{host}}", @@ -1790,27 +1739,14 @@ "opksshOpenBrowser": "Abra o navegador para autenticar.", "opksshWaitingForAuth": "Aguardando autenticação no navegador...", "opksshAuthenticating": "Processando autenticação...", - "opksshTimeout": "A autenticação expirou. Tente novamente.", - "opksshAuthFailed": "A autenticação falhou. Verifique suas credenciais e tente novamente.", "opksshSignInWith": "Faça login com {{provider}}", - "vaultAuthTitle": "É necessário fazer login no cofre.", - "vaultAuthDescription": "Uma janela foi aberta para fazer login no HashiCorp Vault. Conclua o login; esta conexão será estabelecida automaticamente.", - "vaultAuthFailed": "A autenticação do cofre falhou. Tente novamente.", - "vaultReopen": "Reabrir janela de login", "sudoPasswordPopupTitle": "Digite a senha?", - "linkDialogTitle": "Abrir link", - "linkDialogOpen": "Abrir", - "linkDialogCopy": "Cópia", - "websocketAbnormalClose": "A conexão foi encerrada inesperadamente. Isso pode ter ocorrido devido a um problema de configuração de proxy reverso ou SSL. Verifique os registros do servidor.", "connectionLogTitle": "Registro de conexão", "connectionLogCopy": "Copiar registros para a área de transferência", "connectionLogEmpty": "Ainda não há registros de conexão.", "connectionLogWaiting": "Aguardando os registros de conexão...", "connectionLogCopied": "Registros de conexão copiados para a área de transferência", "connectionLogCopyFailed": "Falha ao copiar os registros para a área de transferência", - "connectionRejected": "Conexão rejeitada pelo servidor. Verifique sua autenticação e configuração de rede.", - "hostKeyRejected": "Verificação da chave SSH do host rejeitada. Conexão cancelada.", - "sessionTakenOver": "A sessão foi aberta em outra aba. Reconectando...", "split": { "splitTab": "Aba dividida", "addToSplit": "Adicionar à divisão", @@ -2225,55 +2161,6 @@ "sectionApiKeys": "Chaves de API", "sectionAuditLog": "Registro de auditoria", "sectionSsl": "SSL / Let's Encrypt", - "sectionTouchInput": "Entrada por toque", - "touchEnabled": "Ativar entrada por toque", - "touchEnabledDesc": "Permitir rolagem com um único dedo em terminais.", - "touchMomentumEnabled": "Ativar o impulso", - "touchMomentumEnabledDesc": "Continue rolando brevemente após deslizar o dedo.", - "touchAdvanced": "Avançado", - "touchDragThreshold": "Limiar de arrasto", - "touchMaxWheelDelta": "Delta máximo da roda", - "touchMomentumSampleWindow": "Janela de amostra de momento", - "touchReleaseGracePeriod": "Período de carência para lançamento", - "touchMinimumVelocity": "Velocidade mínima", - "touchMaximumVelocity": "Velocidade máxima", - "touchMaximumDuration": "Duração máxima", - "touchMaximumTravel": "Viagem máxima", - "touchDecayTime": "Tempo de decaimento", - "touchPixelsPerTick": "Pixels por tick", - "touchMaximumFrameInterval": "Intervalo máximo entre quadros", - "touchMaximumTicksPerFrame": "Número máximo de ticks por quadro", - "touchResetDefaults": "Restaurar padrões de fábrica", - "touchSaved": "Configurações de entrada por toque salvas", - "touchSaveFailed": "Falha ao salvar as configurações de entrada por toque", - "sectionImageStorage": "Armazenamento de imagens", - "imageStorageMode": "Modo de armazenamento", - "imageStorageModeDesc": "Local onde as imagens de terminal coladas e carregadas são exibidas. As seleções automáticas por conexão, Local e Remota (SFTP), foram corrigidas.", - "imageStorageModeAuto": "Auto", - "imageStorageModeLocal": "Local", - "imageStorageModeRemoteSftp": "Remoto (SFTP)", - "imageStorageLocalDir": "Diretório local", - "imageStorageLocalDirDesc": "Caminho absoluto no painel administrativo do Termix. Não é exibido após salvar por motivos de segurança.", - "imageStorageLocalDirPlaceholder": "Deixe em branco para manter atualizado.", - "imageStorageHostPath": "Caminho do host", - "imageStorageHostPathDesc": "Visão do diretório local recebida pelos agentes de terminal, do lado do host.", - "imageStorageTtl": "TTL (ms)", - "imageStorageMaxCount": "Contagem máxima de imagens", - "imageStorageMaxBytes": "Bytes de armazenamento máximo", - "imageStorageSaved": "Configurações de armazenamento de imagens salvas", - "imageStorageSaveFailed": "Falha ao salvar as configurações de armazenamento de imagens", - "imageStorageInstanceId": "ID da instância da sessão do terminal", - "imageStorageInstanceIdDesc": "ID da instância de uma das suas abas de terminal conectadas, usada para testar a visibilidade.", - "imageStorageInstanceIdRequired": "Insira primeiro o ID da instância da sessão do terminal.", - "imageStorageTest": "Teste", - "imageStorageTesting": "Testando...", - "imageStorageTestFailed": "Falha ao testar o armazenamento de imagens", - "imageStorageTestConnected": "A sessão do terminal está conectada.", - "imageStorageTestNotConnected": "Nenhuma sessão de terminal conectada com este ID de instância.", - "imageStorageTestSelectedMode": "Um upload usaria: {{mode}}", - "imageStorageSelectedModeUnavailable": "Indisponível", - "imageStorageTestLocalVisible": "O diretório local está visível a partir do host conectado.", - "imageStorageTestLocalNotVisible": "O diretório local NÃO está visível a partir do host conectado.", "sectionBranding": "Marca Branca", "brandingAppName": "Nome do aplicativo", "brandingAppNameDesc": "Exibido na tela de login e na aba do navegador. Deixe em branco para usar o \"Termix\".", @@ -2354,9 +2241,6 @@ "oidcSilentLoginDefaultLockedDesc": "Essa configuração está bloqueada pela variável de ambiente OIDC_SILENT_LOGIN_DEFAULT e não pode ser alterada aqui.", "allowPasswordReset": "Permitir redefinição de senha", "allowPasswordResetDesc": "Redefinir código através dos logs do Docker", - "commandHistoryEnabled": "Histórico de comandos", - "commandHistoryEnabledDesc": "Permitir o registro do histórico de comandos. Quando desativado, o histórico não é salvo, independentemente das configurações de cada host.", - "updateCommandHistoryFailed": "Falha ao atualizar a configuração do histórico de comandos", "analyticsEnabled": "Compartilhar estatísticas de uso anônimas", "analyticsEnabledDesc": "Envia uma contagem diária anônima de usuários, hosts e uso de recursos para ajudar a melhorar o Termix. Nenhum dado pessoal ou detalhe de conexão é incluído.", "analyticsEnabledLockedDesc": "Essa configuração está bloqueada pela variável de ambiente ENABLE_TELEMETRY e não pode ser alterada aqui.", @@ -2647,11 +2531,6 @@ "hostDefaultsTerminal": "terminal", "hostDefaultsAutoTmux": "Auto-Tmux", "hostDefaultsAutoTmuxDesc": "Execute os terminais dos novos hosts dentro do tmux para que as sessões sobrevivam a desconexões (requer tmux no host).", - "terminalSessionTimeout": "Persistência de sessão do terminal", - "terminalSessionTimeoutDesc": "Tempo que um terminal desconectado permanece ativo no servidor antes de ser fechado. Mínimo 1 minuto · Máximo 1440 minutos. Para sessões que precisam durar mais do que isso, habilite o Auto-Tmux no host.", - "terminalSessionTimeoutRange": "Insira um valor entre 1 e 1440 minutos.", - "terminalSessionTimeoutSaved": "Persistência da sessão do terminal salva", - "terminalSessionTimeoutSaveFailed": "Falha ao salvar a persistência da sessão do terminal.", "minutes": "minutos", "hostDefaultsSessionLogging": "Registro de sessão", "hostDefaultsSessionLoggingDesc": "Por padrão, as sessões de terminal em novos hosts são gravadas por padrão.", @@ -2708,15 +2587,6 @@ "connectToTerminal": "Conectar ao terminal", "connectToFiles": "Conectar aos arquivos" }, - "history": { - "noTerminalSelected": "Nenhum terminal selecionado", - "noTerminalSelectedHint": "Abra uma aba do terminal SSH para visualizar seu histórico de comandos.", - "searchPlaceholder": "Histórico de pesquisa...", - "clearAll": "Limpar tudo", - "noHistoryEntries": "Nenhuma entrada no histórico.", - "trackingDisabled": "O rastreamento do histórico está desativado.", - "trackingDisabledHint": "Habilite-o nas configurações do terminal do host." - }, "sshTools": { "keyRecordingTitle": "Gravação principal", "recordToTerminals": "Gravar nos terminais", @@ -3178,30 +3048,11 @@ "terminalToolbar": { "title": "Barra de ferramentas do terminal", "showActions": "Exibir barra de ferramentas", - "hideToolbar": "Ocultar barra de ferramentas", - "showToolbar": "Expandir barra de ferramentas", - "moveToolbar": "Mover barra de ferramentas", - "moveToolbarHint": "Arraste para reposicionar a barra de ferramentas.", - "layout": "Modo de exibição da barra de ferramentas", - "layoutIcon": "Apenas o ícone", - "layoutLabeled": "Ícones e rótulos", - "layoutExpanded": "Expandido com métricas", - "image": "Ações de imagem (somente imagens)", - "upload": "Carregar imagem", - "paste": "Colar imagem", - "detachTmux": "Desanexar tmux", - "detachTmuxDescription": "Desanexar sessão tmux", - "imageActionFailed": "A ação de imagem falhou", - "retry": "Tentar novamente", - "dismissError": "Ignorar erro de imagem", "cpu": "CPU", "memory": "Memória", "disk": "Disco", "statsUnavailable": "Estatísticas ao vivo indisponíveis", "tmuxDetach": "Desanexar sessão tmux", - "uploadImage": "Faça o upload de uma imagem para o host.", - "uploadingImage": "Carregando...", - "pasteImage": "Colar imagem da área de transferência (somente imagens)", "openFiles": "Abrir arquivos", "openDocker": "Abra o Docker", "openTunnel": "Túnel aberto", @@ -3285,31 +3136,6 @@ "timeHours": "{{count}}h atrás", "timeDays": "{{count}}há alguns dias" }, - "mobileKeyboard": { - "shift": "Mudança", - "ctrl": "Ctrl", - "esc": "Esc", - "tab": "Aba", - "backTab": "⇥", - "arrowUp": "Seta para cima", - "arrowDown": "Seta para baixo", - "arrowLeft": "Seta para a esquerda", - "arrowRight": "Seta para a direita", - "home": "Lar", - "end": "Fim", - "pageUp": "PgUp", - "pageDown": "PgDn", - "delete": "Del", - "paste": "Colar", - "editQuickKeys": "Editar teclas de atalho", - "quickKeysTitle": "Teclas de atalho", - "quickKeysDesc": "Toque em × para remover. Suporta até 8 caracteres.", - "quickKeyPlaceholder": "por exemplo sudo ", - "addQuickKey": "Adicionar", - "removeQuickKey": "Remover", - "resetDefaults": "Restaurar padrões de fábrica", - "done": "Feito" - }, "serial": { "title": "Serial", "portLabel": "Porta", diff --git a/src/ui/locales/translated/pt_PT.json b/src/ui/locales/translated/pt_PT.json index 513083b5d..ac4065383 100644 --- a/src/ui/locales/translated/pt_PT.json +++ b/src/ui/locales/translated/pt_PT.json @@ -742,7 +742,6 @@ "addHost": "Adicionar máquina", "editHost": "Editar máquina", "cloneHost": "Clonar máquina", - "enableTerminal": "Ativar Terminal", "enableTunnel": "Ativar Túnel", "enableFileManager": "Ativar Gestor de Ficheiros", "enableDocker": "Ativar Docker", @@ -1041,8 +1040,6 @@ "enableSessionLoggingDesc": "Gravar a saída da sessão de terminal para revisão posterior", "allowSessionSharing": "Permitir partilha de sessão", "allowSessionSharingDesc": "Permitir que as sessões em direto neste servidor sejam partilhadas através de link ou com outros utilizadores.", - "enableCommandHistory": "Histórico de comandos", - "enableCommandHistoryDesc": "Gravar comandos executados neste terminal para histórico e autocompletar", "localEchoLabel": "Eco Local", "localEchoDesc": "Preveja sequências de teclas seguras e imprimíveis localmente em ligações lentas. Os avisos de palavra-passe e as entradas de controlo nunca são repetidos.", "localEchoDefault": "Usar configuração global", @@ -1072,8 +1069,6 @@ "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", - "enableTerminalToolbar": "Ativar a barra de ferramentas do terminal", - "enableTerminalToolbarDesc": "Visualizar uma barra de ferramentas de acesso rápido acoplada ao terminal ativo com as ferramentas e estatísticas em tempo real deste host.", "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.", @@ -1550,9 +1545,7 @@ "links": "Ligações", "navigate": "Navegar", "select": "Selecionar", - "toggleWith": "Alternar com", - "localTerminal": "Terminal local", - "localTerminalDesc": "Abra um terminal neste computador." + "toggleWith": "Alternar com" }, "splitScreen": { "paneEmpty": "Painel {{index}} - vazio", @@ -1717,57 +1710,13 @@ "loadSharesFailed": "Falha ao carregar partilhas ativas" }, "terminal": { - "sessionExpiredNotice": "A sua sessão anterior em {{host}} expirou após {{minutes}} minutos desligado; o shell e tudo o que estava a ser executado nele foram perdidos. Active o Auto-Tmux neste host para manter as sessões ativas.", - "sessionExpiredNoticeNoMinutes": "A sua sessão anterior em {{host}} expirou enquanto estava desligada; o shell e tudo o que estava a ser executado nele foram perdidos. Active o Auto-Tmux neste host para manter as sessões ativas.", - "enableAutoTmuxAction": "Ativar o Auto-Tmux", - "autoTmuxEnabled": "Auto-Tmux ativado para {{host}}. Aplica-se a partir da ligação seguinte.", - "autoTmuxEnableFailed": "Falha ao ativar o Auto-Tmux", "connect": "Ligar ao Host", "clear": "Limpar", "paste": "Colar", - "reconnect": "Reconectar", - "connectionLost": "Ligação perdida", "connected": "Ligado", "clipboardWriteFailed": "Erro ao copiar para a área de transferência. Certifique-se de que a página é servida via HTTPS ou localhost.", "clipboardReadFailed": "Erro ao ler da área de transferência. Certifique-se de que as permissões da área de transferência foram concedidas.", - "clipboardHttpWarning": "Não foi possível ler a área de transferência. Alguns browsers restringem o acesso à área de transferência fora de HTTPS - experimente Ctrl+V ou utilize HTTPS para aceder ao Termix.", - "passwordPromptFillTitle": "Preencher a palavra-passe guardada neste pedido?", - "unknownError": "Ocorreu um erro desconhecido.", - "websocketError": "Erro de ligação WebSocket", - "connecting": "A ligar...", - "noHostSelected": "Nenhum host selecionado", - "reconnecting": "A restabelecer ligação... ({{attempt}}/{{max}})", - "reconnected": "Ligação restabelecida com sucesso", - "tmuxSessionCreated": "Sessão tmux criada: {{name}}", - "tmuxSessionAttached": "Sessão tmux anexada: {{name}}", - "tmuxUnavailable": "O tmux não está instalado no host remoto, a reverter para shell padrão", - "tmuxSessionPickerTitle": "Sessões tmux", - "tmuxSessionPickerDesc": "Foram encontradas sessões tmux neste host. Selecione uma para retomar ou crie uma nova sessão.", - "tmuxWindows": "Janelas", - "tmuxWindowCount": "{{count}} janela", - "tmuxAttached": "Clientes anexados", - "tmuxAttachedCount": "{{count}} anexado", - "tmuxLastActivity": "Última atividade", - "tmuxTimeJustNow": "agora mesmo", - "tmuxTimeMinutes": "há {{count}} min", - "tmuxTimeHours": "há {{count}} h", - "tmuxTimeDays": "há {{count}} d", - "tmuxCreateNew": "Iniciar nova sessão", - "tmuxCopyHint": "Ajuste a seleção e prima Enter para copiar para a área de transferência", "tmuxDetach": "Desanexar da sessão tmux", - "tmuxDetached": "Desanexado da sessão tmux", - "searchPlaceholder": "Encontrar", - "searchCaseSensitive": "Caixa de fósforos", - "searchWholeWord": "Combine a palavra inteira", - "searchRegex": "Utilizar expressão regular", - "searchNoResults": "Nenhum resultado", - "searchResultCount": "{{index}} de {{count}}", - "searchNext": "Próxima partida (Entrar)", - "searchPrevious": "Partida anterior (Shift+Enter)", - "searchClose": "Fechar (Esc)", - "maxReconnectAttemptsReached": "Número máximo de tentativas de restabelecimento de ligação atingido", - "closeTab": "Fechar", - "connectionTimeout": "Tempo limite da ligação", "terminalTitle": "Terminal - {{host}}", "terminalWithPath": "Terminal - {{host}}:{{path}}", "runTitle": "A executar {{command}} - {{host}}", @@ -1790,27 +1739,14 @@ "opksshOpenBrowser": "Abrir navegador para autenticar", "opksshWaitingForAuth": "A aguardar autenticação no navegador...", "opksshAuthenticating": "A processar autenticação...", - "opksshTimeout": "A autenticação expirou. Tente novamente.", - "opksshAuthFailed": "A autenticação falhou. Verifique as suas credenciais e tente novamente.", "opksshSignInWith": "Iniciar sessão com {{provider}}", - "vaultAuthTitle": "Início de sessão no Vault necessário", - "vaultAuthDescription": "Foi aberta uma janela para iniciar sessão no HashiCorp Vault. Conclua o início de sessão aí; esta ligação continuará automaticamente.", - "vaultAuthFailed": "A autenticação no Vault falhou. Tente novamente.", - "vaultReopen": "Reabrir janela de início de sessão", "sudoPasswordPopupTitle": "Inserir Password?", - "linkDialogTitle": "Abrir Ligação", - "linkDialogOpen": "Abrir", - "linkDialogCopy": "Copiar", - "websocketAbnormalClose": "A ligação foi fechada inesperadamente. Isto pode dever-se a um problema de configuração do proxy reverso ou SSL. Verifique os registos do servidor.", "connectionLogTitle": "Registo da Ligação", "connectionLogCopy": "Copiar registos para a área de transferência", "connectionLogEmpty": "Ainda não há registos da ligação", "connectionLogWaiting": "A aguardar registos da ligação...", "connectionLogCopied": "Registos da ligação copiados para a área de transferência", "connectionLogCopyFailed": "Falha ao copiar registos para a área de transferência", - "connectionRejected": "Ligação rejeitada pelo servidor. Verifique a sua autenticação e configuração de rede.", - "hostKeyRejected": "Verificação da chave de host SSH rejeitada. Ligação cancelada.", - "sessionTakenOver": "A sessão foi aberta noutro separador. A restabelecer ligação...", "split": { "splitTab": "Dividir Separador", "addToSplit": "Adicionar à Divisão", @@ -2225,55 +2161,6 @@ "sectionApiKeys": "Chaves API", "sectionAuditLog": "Registo de auditoria", "sectionSsl": "SSL / Let's Encrypt", - "sectionTouchInput": "Entrada por toque", - "touchEnabled": "Ativar entrada por toque", - "touchEnabledDesc": "Permitir a rolagem com um único dedo nos terminais.", - "touchMomentumEnabled": "Ativar o impulso", - "touchMomentumEnabledDesc": "Continue a rolar brevemente após deslizar o dedo.", - "touchAdvanced": "Avançado", - "touchDragThreshold": "Limiar de arrasto", - "touchMaxWheelDelta": "Delta máximo da roda", - "touchMomentumSampleWindow": "Janela de amostra de momento", - "touchReleaseGracePeriod": "Período de carência para lançamento", - "touchMinimumVelocity": "Velocidade mínima", - "touchMaximumVelocity": "Velocidade máxima", - "touchMaximumDuration": "Duração máxima", - "touchMaximumTravel": "Viagem máxima", - "touchDecayTime": "Tempo de decaimento", - "touchPixelsPerTick": "Pixels por tick", - "touchMaximumFrameInterval": "Intervalo máximo entre fotogramas", - "touchMaximumTicksPerFrame": "Número máximo de ticks por frame", - "touchResetDefaults": "Restaurar as predefinições de fábrica", - "touchSaved": "Configurações de entrada por toque guardadas", - "touchSaveFailed": "Falha ao guardar as definições de entrada por toque", - "sectionImageStorage": "Armazenamento de imagens", - "imageStorageMode": "Modo de armazenamento", - "imageStorageModeDesc": "Local onde são apresentadas as imagens de terminal coladas e carregadas. As seleções automáticas por ligação, Local e Remota (SFTP), foram corrigidas.", - "imageStorageModeAuto": "Auto", - "imageStorageModeLocal": "Local", - "imageStorageModeRemoteSftp": "Remoto (SFTP)", - "imageStorageLocalDir": "Diretório local", - "imageStorageLocalDirDesc": "Caminho absoluto no painel administrativo do Termix. Não é apresentado após guardar por motivos de segurança.", - "imageStorageLocalDirPlaceholder": "Deixe em branco para manter atualizado.", - "imageStorageHostPath": "Caminho do host", - "imageStorageHostPathDesc": "Vista do diretório local recebida pelos agentes de terminal, do lado do host.", - "imageStorageTtl": "TTL (ms)", - "imageStorageMaxCount": "Contagem máxima de imagens", - "imageStorageMaxBytes": "Bytes de armazenamento máximo", - "imageStorageSaved": "Configurações de armazenamento de imagens guardadas", - "imageStorageSaveFailed": "Falha ao guardar as definições de armazenamento de imagens", - "imageStorageInstanceId": "ID da instância da sessão do terminal", - "imageStorageInstanceIdDesc": "ID da instância de uma das suas abas de terminal ligadas, utilizada para testar a visibilidade.", - "imageStorageInstanceIdRequired": "Introduza primeiro o ID da instância da sessão do terminal.", - "imageStorageTest": "Teste", - "imageStorageTesting": "Testando...", - "imageStorageTestFailed": "Falha ao testar o armazenamento de imagens", - "imageStorageTestConnected": "A sessão do terminal está ligada.", - "imageStorageTestNotConnected": "Nenhuma sessão de terminal ligada com este ID de instância.", - "imageStorageTestSelectedMode": "Um upload utilizaria: {{mode}}", - "imageStorageSelectedModeUnavailable": "Indisponível", - "imageStorageTestLocalVisible": "O diretório local está visível a partir do host ligado.", - "imageStorageTestLocalNotVisible": "O diretório local NÃO está visível a partir do host ligado.", "sectionBranding": "Marca Branca", "brandingAppName": "Nome do aplicativo", "brandingAppNameDesc": "Exibido no ecrã de login e na aba do navegador. Deixe em branco para utilizar o \"Termix\".", @@ -2354,9 +2241,6 @@ "oidcSilentLoginDefaultLockedDesc": "Esta configuração está bloqueada pela variável de ambiente OIDC_SILENT_LOGIN_DEFAULT e não pode ser alterada aqui.", "allowPasswordReset": "Permitir Reposição de Palavra-passe", "allowPasswordResetDesc": "Código de reposição via registos do Docker", - "commandHistoryEnabled": "Histórico de Comandos", - "commandHistoryEnabledDesc": "Permitir o registo do histórico de comandos. Quando desativado, o histórico não é guardado independentemente das definições por máquina.", - "updateCommandHistoryFailed": "Falha ao atualizar a definição de histórico de comandos", "analyticsEnabled": "Partilhar estatísticas de uso anónimas", "analyticsEnabledDesc": "Envia uma contagem diária anónima de utilizadores, hosts e utilização de recursos para ajudar a melhorar o Termix. Nenhum dado pessoal ou detalhe de ligação é incluído.", "analyticsEnabledLockedDesc": "Esta definição está bloqueada pela variável de ambiente ENABLE_TELEMETRY e não pode ser alterada aqui.", @@ -2647,11 +2531,6 @@ "hostDefaultsTerminal": "Terminal", "hostDefaultsAutoTmux": "Auto-Tmux", "hostDefaultsAutoTmuxDesc": "Execute os terminais dos novos hosts dentro do tmux para que as sessões sobrevivam a desconexões (requer tmux no host).", - "terminalSessionTimeout": "Persistência de sessão do terminal", - "terminalSessionTimeoutDesc": "Tempo que um terminal desligado permanece ativo no servidor antes de ser fechado. Mínimo 1 minuto · Máximo 1440 minutos. Para sessões que necessitem de durar mais do que isso, ative o Auto-Tmux no host.", - "terminalSessionTimeoutRange": "Introduza um valor entre 1 e 1440 minutos.", - "terminalSessionTimeoutSaved": "Persistência da sessão do terminal guardada", - "terminalSessionTimeoutSaveFailed": "Falha ao guardar a persistência da sessão do terminal.", "minutes": "minutos", "hostDefaultsSessionLogging": "Registo de Sessão", "hostDefaultsSessionLoggingDesc": "Gravar sessões de terminal nos novos hosts por predefinição", @@ -2708,15 +2587,6 @@ "connectToTerminal": "Ligar ao Terminal", "connectToFiles": "Ligar aos Ficheiros" }, - "history": { - "noTerminalSelected": "Nenhum terminal selecionado", - "noTerminalSelectedHint": "Abra um separador de terminal SSH para ver o histórico de comandos", - "searchPlaceholder": "Pesquisar histórico...", - "clearAll": "Limpar Tudo", - "noHistoryEntries": "Sem entradas no histórico", - "trackingDisabled": "O rastreio do histórico está desativado", - "trackingDisabledHint": "Ative-o nas definições do terminal do anfitrião." - }, "sshTools": { "keyRecordingTitle": "Gravação de Teclas", "recordToTerminals": "Gravar para terminais", @@ -3178,30 +3048,11 @@ "terminalToolbar": { "title": "Barra de ferramentas do terminal", "showActions": "Exibir barra de ferramentas", - "hideToolbar": "Ocultar barra de ferramentas", - "showToolbar": "Expandir barra de ferramentas", - "moveToolbar": "Mover barra de ferramentas", - "moveToolbarHint": "Arraste para reposicionar a barra de ferramentas.", - "layout": "Modo de visualização da barra de ferramentas", - "layoutIcon": "Apenas o ícone", - "layoutLabeled": "Ícones e rótulos", - "layoutExpanded": "Expandido com métricas", - "image": "Ações de imagem (apenas imagens)", - "upload": "Carregar imagem", - "paste": "Colar imagem", - "detachTmux": "Desanexar tmux", - "detachTmuxDescription": "Desanexar sessão tmux", - "imageActionFailed": "A ação de imagem falhou", - "retry": "Tentar novamente", - "dismissError": "Ignorar erro de imagem", "cpu": "CPU", "memory": "Memória", "disk": "Disco", "statsUnavailable": "Estatísticas ao vivo indisponíveis", "tmuxDetach": "Desanexar sessão tmux", - "uploadImage": "Faça o upload de uma imagem para o host.", - "uploadingImage": "Carregando...", - "pasteImage": "Colar imagem da área de transferência (apenas imagens)", "openFiles": "Abrir ficheiros", "openDocker": "Abra o Docker", "openTunnel": "Túnel aberto", @@ -3285,31 +3136,6 @@ "timeHours": "há {{count}} h", "timeDays": "há {{count}} d" }, - "mobileKeyboard": { - "shift": "Shift", - "ctrl": "Ctrl", - "esc": "Esc", - "tab": "Tab", - "backTab": "⇥", - "arrowUp": "Seta para Cima", - "arrowDown": "Seta para Baixo", - "arrowLeft": "Seta para a Esquerda", - "arrowRight": "Seta para a Direita", - "home": "Início", - "end": "Fim", - "pageUp": "PgUp", - "pageDown": "PgDn", - "delete": "Del", - "paste": "Colar", - "editQuickKeys": "Editar teclas rápidas", - "quickKeysTitle": "Teclas Rápidas", - "quickKeysDesc": "Toque no × para remover. Suporta até 8 caracteres.", - "quickKeyPlaceholder": "ex: sudo ", - "addQuickKey": "Adicionar", - "removeQuickKey": "Remover", - "resetDefaults": "Restaurar predefinições", - "done": "Concluir" - }, "serial": { "title": "Serial", "portLabel": "Porta", diff --git a/src/ui/locales/translated/ro_RO.json b/src/ui/locales/translated/ro_RO.json index 7791e034e..28fea3903 100644 --- a/src/ui/locales/translated/ro_RO.json +++ b/src/ui/locales/translated/ro_RO.json @@ -742,7 +742,6 @@ "addHost": "Adăugați gazdă", "editHost": "Editați gazda", "cloneHost": "Gazdă clonată", - "enableTerminal": "Activează terminalul", "enableTunnel": "Activează tunelul", "enableFileManager": "Activează Managerul de fișiere", "enableDocker": "Activează Docker", @@ -1041,8 +1040,6 @@ "enableSessionLoggingDesc": "Înregistrați ieșirea sesiunii terminale pentru o revizuire ulterioară", "allowSessionSharing": "Permiteți partajarea sesiunii", "allowSessionSharingDesc": "Permiteți partajarea sesiunilor live de pe această gazdă prin link sau cu alți utilizatori", - "enableCommandHistory": "Istoricul comenzilor", - "enableCommandHistoryDesc": "Înregistrați comenzile executate în acest terminal pentru istoric și completare automată", "localEchoLabel": "Ecou local", "localEchoDesc": "Preziceți local apăsările de taste imprimabile în siguranță pe conexiuni lente. Solicitările de parolă și intrările de control nu sunt niciodată reluate.", "localEchoDefault": "Folosește setarea globală", @@ -1072,8 +1069,6 @@ "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ă", - "enableTerminalToolbar": "Activează bara de instrumente Terminal", - "enableTerminalToolbarDesc": "Afișează o bară de instrumente cu acces rapid atașată la terminalul activ pentru instrumentele și statisticile live ale acestei gazde", "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.", @@ -1550,9 +1545,7 @@ "links": "Linkuri", "navigate": "Navigare", "select": "Selecta", - "toggleWith": "Comutare cu", - "localTerminal": "Terminal local", - "localTerminalDesc": "Deschideți o shell pe acest computer" + "toggleWith": "Comutare cu" }, "splitScreen": { "paneEmpty": "Panou {{index}} - gol", @@ -1717,57 +1710,13 @@ "loadSharesFailed": "Nu s-au putut încărca partajările active" }, "terminal": { - "sessionExpiredNotice": "Sesiunea anterioară de pe {{host}} a expirat după {{minutes}} minute de deconectare; shell-ul și tot ce rulează în el au dispărut. Activați Auto-Tmux pe această gazdă pentru a menține sesiunile active.", - "sessionExpiredNoticeNoMinutes": "Sesiunea anterioară pe {{host}} a expirat în timp ce era deconectat; shell-ul și tot ce rulează în el au dispărut. Activați Auto-Tmux pe această gazdă pentru a menține sesiunile active.", - "enableAutoTmuxAction": "Activează Auto-Tmux", - "autoTmuxEnabled": "Auto-Tmux activat pentru {{host}}. Se aplică de la următoarea conexiune.", - "autoTmuxEnableFailed": "Nu s-a putut activa Auto-Tmux", "connect": "Conectare la gazdă", "clear": "Clar", "paste": "Pastă", - "reconnect": "Reconectați-vă", - "connectionLost": "Conexiune pierdută", "connected": "Conectat", "clipboardWriteFailed": "Copierea în clipboard nu a reușit. Asigurați-vă că pagina este servită prin HTTPS sau localhost.", "clipboardReadFailed": "Citirea din clipboard nu a reușit. Asigurați-vă că permisiunile de acces la clipboard sunt acordate.", - "clipboardHttpWarning": "Nu s-a putut citi clipboard-ul. Unele browsere restricționează accesul la clipboard în afara HTTPS - încercați Ctrl+V sau serviți Termix prin HTTPS.", - "passwordPromptFillTitle": "Introduceți parola salvată în această solicitare?", - "unknownError": "A apărut o eroare necunoscută", - "websocketError": "Eroare de conexiune WebSocket", - "connecting": "Se conectează...", - "noHostSelected": "Nicio gazdă selectată", - "reconnecting": "Se reconecta... ({{attempt}}/{{max}})", - "reconnected": "Reconectare cu succes", - "tmuxSessionCreated": "Sesiune tmux creată: {{name}}", - "tmuxSessionAttached": "sesiune tmux atașată: {{name}}", - "tmuxUnavailable": "tmux nu este instalat pe gazda la distanță, revenind la shell-ul standard", - "tmuxSessionPickerTitle": "Sesiuni tmux", - "tmuxSessionPickerDesc": "Au fost găsite sesiuni tmux existente pe această gazdă. Selectați una pentru a o reatașa sau pentru a crea o sesiune nouă.", - "tmuxWindows": "Ferestre", - "tmuxWindowCount": "fereastră {{count}}", - "tmuxAttached": "Clienți atașați", - "tmuxAttachedCount": "{{count}} atașat", - "tmuxLastActivity": "Ultima activitate", - "tmuxTimeJustNow": "chiar acum", - "tmuxTimeMinutes": "{{count}}acum câteva minute", - "tmuxTimeHours": "{{count}}acum o oră", - "tmuxTimeDays": "{{count}}zile în urmă", - "tmuxCreateNew": "Începeți o sesiune nouă", - "tmuxCopyHint": "Ajustați selecția și apăsați Enter pentru a copia în clipboard", "tmuxDetach": "Detașare de la sesiunea tmux", - "tmuxDetached": "Detașat de sesiunea tmux", - "searchPlaceholder": "Găsi", - "searchCaseSensitive": "Potrivire caz", - "searchWholeWord": "Potrivire cuvânt întreg", - "searchRegex": "Folosește expresia regulată", - "searchNoResults": "Niciun rezultat", - "searchResultCount": "{{index}} din {{count}}", - "searchNext": "Următorul meci (Introduce)", - "searchPrevious": "Potrivire anterioară (Shift+Enter)", - "searchClose": "Închide (Escape)", - "maxReconnectAttemptsReached": "S-a atins numărul maxim de încercări de reconectare", - "closeTab": "Aproape", - "connectionTimeout": "Expirare conexiune", "terminalTitle": "Terminal - {{host}}", "terminalWithPath": "Terminal - {{host}}:{{path}}", "runTitle": "Rulează {{command}} - {{host}}", @@ -1790,27 +1739,14 @@ "opksshOpenBrowser": "Deschideți browserul pentru autentificare", "opksshWaitingForAuth": "Se așteaptă autentificarea în browser...", "opksshAuthenticating": "Se procesează autentificarea...", - "opksshTimeout": "Autentificarea a expirat. Vă rugăm să încercați din nou.", - "opksshAuthFailed": "Autentificarea a eșuat. Verificați acreditările și încercați din nou.", "opksshSignInWith": "Conectați-vă cu {{provider}}", - "vaultAuthTitle": "Este necesară conectarea la seif", - "vaultAuthDescription": "S-a deschis o fereastră pentru a vă conecta la HashiCorp Vault. Finalizați conectarea acolo; această conexiune va continua automat.", - "vaultAuthFailed": "Autentificarea în seif a eșuat. Vă rugăm să încercați din nou.", - "vaultReopen": "Redeschideți fereastra de conectare", "sudoPasswordPopupTitle": "Introduceți parola?", - "linkDialogTitle": "Deschideți linkul", - "linkDialogOpen": "Deschide", - "linkDialogCopy": "Copie", - "websocketAbnormalClose": "Conexiunea s-a închis pe neașteptate. Acest lucru se poate datora unei probleme de configurare a proxy-ului invers sau a SSL. Vă rugăm să verificați jurnalele serverului.", "connectionLogTitle": "Jurnal de conectare", "connectionLogCopy": "Copiați jurnalele în clipboard", "connectionLogEmpty": "Încă nu există jurnale de conectare", "connectionLogWaiting": "Se așteaptă jurnalele de conectare...", "connectionLogCopied": "Jurnalele de conexiune au fost copiate în clipboard", "connectionLogCopyFailed": "Nu s-au putut copia jurnalele în clipboard", - "connectionRejected": "Conexiune respinsă de server. Vă rugăm să verificați autentificarea și configurația rețelei.", - "hostKeyRejected": "Verificarea cheii gazdei SSH a fost respinsă. Conexiunea a fost anulată.", - "sessionTakenOver": "Sesiunea a fost deschisă într-o altă filă. Se reconectare...", "split": { "splitTab": "Tabulă divizată", "addToSplit": "Adăugați la divizare", @@ -2225,55 +2161,6 @@ "sectionApiKeys": "Chei API", "sectionAuditLog": "Jurnal de audit", "sectionSsl": "SSL / Să criptăm", - "sectionTouchInput": "Intrare tactilă", - "touchEnabled": "Activează introducerea tactilă", - "touchEnabledDesc": "Permiteți derularea tactilă cu un singur deget în terminale", - "touchMomentumEnabled": "Activează impulsul", - "touchMomentumEnabledDesc": "Continuați derularea scurtă după o glisare", - "touchAdvanced": "Avansat", - "touchDragThreshold": "Pragul de rezistență", - "touchMaxWheelDelta": "Delta maximă a roții", - "touchMomentumSampleWindow": "Fereastra de eșantionare a impulsului", - "touchReleaseGracePeriod": "Perioada de grație a lansării", - "touchMinimumVelocity": "Viteză minimă", - "touchMaximumVelocity": "Viteză maximă", - "touchMaximumDuration": "Durată maximă", - "touchMaximumTravel": "Cursă maximă", - "touchDecayTime": "Timp de descreștere", - "touchPixelsPerTick": "Pixeli per bifă", - "touchMaximumFrameInterval": "Interval maxim de cadre", - "touchMaximumTicksPerFrame": "Număr maxim de ticuri per cadru", - "touchResetDefaults": "Resetare la setările implicite", - "touchSaved": "Setările de introducere tactilă au fost salvate", - "touchSaveFailed": "Setările de introducere tactilă nu au putut fi salvate.", - "sectionImageStorage": "Stocarea imaginilor", - "imageStorageMode": "Mod de stocare", - "imageStorageModeDesc": "Unde sunt scrise imaginile terminalului lipite și încărcate. Selecțiile automate prin conexiune, Local și Remote (SFTP) sunt fixe.", - "imageStorageModeAuto": "Auto", - "imageStorageModeLocal": "Local", - "imageStorageModeRemoteSftp": "La distanță (SFTP)", - "imageStorageLocalDir": "Director local", - "imageStorageLocalDirDesc": "Cale absolută în backend-ul Termix. Nu este afișată după salvare din motive de securitate.", - "imageStorageLocalDirPlaceholder": "Lăsați gol pentru a menține actualizat", - "imageStorageHostPath": "Calea gazdă", - "imageStorageHostPathDesc": "Vizualizarea din partea gazdei a directorului local pe care îl primesc agenții terminalului.", - "imageStorageTtl": "TTL (ms)", - "imageStorageMaxCount": "Număr maxim de imagini", - "imageStorageMaxBytes": "Capacitate maximă de stocare în octeți", - "imageStorageSaved": "Setările de stocare a imaginilor au fost salvate", - "imageStorageSaveFailed": "Setările de stocare a imaginilor nu au putut fi salvate.", - "imageStorageInstanceId": "ID-ul instanței sesiunii terminale", - "imageStorageInstanceIdDesc": "ID-ul instanței uneia dintre filele terminalului conectat, folosit pentru a testa vizibilitatea.", - "imageStorageInstanceIdRequired": "Introduceți mai întâi un ID de instanță al sesiunii terminale", - "imageStorageTest": "Test", - "imageStorageTesting": "Testare...", - "imageStorageTestFailed": "Nu s-a putut testa stocarea imaginilor", - "imageStorageTestConnected": "Sesiunea terminalului este conectată.", - "imageStorageTestNotConnected": "Nicio sesiune de terminal conectată cu acest ID de instanță.", - "imageStorageTestSelectedMode": "O încărcare ar folosi: {{mode}}", - "imageStorageSelectedModeUnavailable": "Indisponibil", - "imageStorageTestLocalVisible": "Directorul local este vizibil de pe gazda conectată.", - "imageStorageTestLocalNotVisible": "Directorul local NU este vizibil de pe gazda conectată.", "sectionBranding": "Marcă albă", "brandingAppName": "Numele aplicației", "brandingAppNameDesc": "Afișat pe ecranul de conectare și în fila browserului. Lăsați câmpul gol pentru a utiliza „Termix”.", @@ -2354,9 +2241,6 @@ "oidcSilentLoginDefaultLockedDesc": "Această setare este blocată de variabila de mediu OIDC_SILENT_LOGIN_DEFAULT și nu poate fi modificată aici.", "allowPasswordReset": "Permite resetarea parolei", "allowPasswordResetDesc": "Resetează codul prin jurnalele Docker", - "commandHistoryEnabled": "Istoricul comenzilor", - "commandHistoryEnabledDesc": "Permite înregistrarea istoricului comenzilor. Când este dezactivată, istoricul nu este salvat, indiferent de setările pentru fiecare gazdă.", - "updateCommandHistoryFailed": "Nu s-a putut actualiza setarea istoricului comenzilor", "analyticsEnabled": "Distribuiți statistici de utilizare anonime", "analyticsEnabledDesc": "Trimite un număr zilnic anonim de utilizatori, gazde și utilizare a funcțiilor pentru a ajuta la îmbunătățirea Termix. Nu sunt incluse niciodată date personale sau detalii de conexiune.", "analyticsEnabledLockedDesc": "Această setare este blocată de variabila de mediu ENABLE_TELEMETRY și nu poate fi modificată aici.", @@ -2647,11 +2531,6 @@ "hostDefaultsTerminal": "Terminal", "hostDefaultsAutoTmux": "Auto-Tmux", "hostDefaultsAutoTmuxDesc": "Rulează terminalele noilor gazde în interiorul tmux, astfel încât sesiunile să supraviețuiască deconectărilor (necesită tmux pe gazdă)", - "terminalSessionTimeout": "Persistența sesiunii terminale", - "terminalSessionTimeoutDesc": "Cât timp este menținut activ pe server un terminal deconectat înainte de a fi închis. Min 1 · Max 1440 de minute. Pentru sesiunile care trebuie să dureze mai mult decât această durată, activați Auto-Tmux pe gazdă.", - "terminalSessionTimeoutRange": "Introduceți o valoare între 1 și 1440 de minute", - "terminalSessionTimeoutSaved": "Persistența sesiunii terminale a fost salvată", - "terminalSessionTimeoutSaveFailed": "Nu s-a putut salva persistența sesiunii terminale", "minutes": "minute", "hostDefaultsSessionLogging": "Înregistrarea sesiunilor", "hostDefaultsSessionLoggingDesc": "Înregistrați sesiunile de terminal pe gazde noi în mod implicit", @@ -2708,15 +2587,6 @@ "connectToTerminal": "Conectați-vă la terminal", "connectToFiles": "Conectare la fișiere" }, - "history": { - "noTerminalSelected": "Niciun terminal selectat", - "noTerminalSelectedHint": "Deschideți o filă de terminal SSH pentru a vizualiza istoricul comenzilor sale", - "searchPlaceholder": "Istoricul căutărilor...", - "clearAll": "Ștergeți tot", - "noHistoryEntries": "Nicio intrare în istoric", - "trackingDisabled": "Urmărirea istoricului este dezactivată", - "trackingDisabledHint": "Activați-l în setările terminalului gazdei." - }, "sshTools": { "keyRecordingTitle": "Înregistrare cheie", "recordToTerminals": "Înregistrare pe terminale", @@ -3178,30 +3048,11 @@ "terminalToolbar": { "title": "Bara de instrumente a terminalului", "showActions": "Afișează bara de instrumente", - "hideToolbar": "Ascunde bara de instrumente", - "showToolbar": "Extindeți bara de instrumente", - "moveToolbar": "Mută bara de instrumente", - "moveToolbarHint": "Trageți pentru a repoziționa bara de instrumente", - "layout": "Mod de afișare a barei de instrumente", - "layoutIcon": "Numai pictogramă", - "layoutLabeled": "Pictograme și etichete", - "layoutExpanded": "Extins cu valori metrice", - "image": "Acțiuni de imagine (doar imagini)", - "upload": "Încărcați imaginea", - "paste": "Lipiți imaginea", - "detachTmux": "Detașați tmux", - "detachTmuxDescription": "Detașați sesiunea tmux", - "imageActionFailed": "Acțiunea imaginii a eșuat", - "retry": "Reîncercați", - "dismissError": "Eroare de închidere a imaginii", "cpu": "CPU", "memory": "Memorie", "disk": "Disc", "statsUnavailable": "Statistici live indisponibile", "tmuxDetach": "Detașați sesiunea tmux", - "uploadImage": "Încărcați o imagine către gazdă", - "uploadingImage": "Se încarcă...", - "pasteImage": "Lipiți imaginea din clipboard (doar imagini)", "openFiles": "Deschideți fișierele", "openDocker": "Deschideți Docker", "openTunnel": "Tunel deschis", @@ -3285,31 +3136,6 @@ "timeHours": "{{count}}acum o oră", "timeDays": "{{count}}zile în urmă" }, - "mobileKeyboard": { - "shift": "Schimbare", - "ctrl": "Ctrl", - "esc": "Esc", - "tab": "Tab", - "backTab": "⇥", - "arrowUp": "Săgeată în sus", - "arrowDown": "Săgeată în jos", - "arrowLeft": "Săgeată stânga", - "arrowRight": "Săgeată dreapta", - "home": "Acasă", - "end": "Sfârşit", - "pageUp": "PagUp", - "pageDown": "Pagină următoare", - "delete": "Șterge", - "paste": "Pastă", - "editQuickKeys": "Editați tastele rapide", - "quickKeysTitle": "Taste rapide", - "quickKeysDesc": "Atingeți × pentru a elimina. Acceptă până la 8 caractere.", - "quickKeyPlaceholder": "de exemplu sudo ", - "addQuickKey": "Adăuga", - "removeQuickKey": "Elimina", - "resetDefaults": "Resetare la setările implicite", - "done": "Făcut" - }, "serial": { "title": "Serial", "portLabel": "Port", diff --git a/src/ui/locales/translated/ru_RU.json b/src/ui/locales/translated/ru_RU.json index a5d16fe4c..7b56e1731 100644 --- a/src/ui/locales/translated/ru_RU.json +++ b/src/ui/locales/translated/ru_RU.json @@ -742,7 +742,6 @@ "addHost": "Добавить хост", "editHost": "Редактировать хост", "cloneHost": "Клонировать хост", - "enableTerminal": "Включить терминал", "enableTunnel": "Включить туннель", "enableFileManager": "Включить файловый менеджер", "enableDocker": "Включить Docker", @@ -1041,8 +1040,6 @@ "enableSessionLoggingDesc": "Записывать вывод терминальной сессии для последующего просмотра", "allowSessionSharing": "Разрешить совместное использование сеанса", "allowSessionSharingDesc": "Разрешите делиться записями онлайн-сессий на этом хостинге по ссылке или с другими пользователями.", - "enableCommandHistory": "История команд", - "enableCommandHistoryDesc": "Сохранять команды, выполняемые в этом терминале, для истории и автодополнения", "localEchoLabel": "Локальное эхо", "localEchoDesc": "Предсказывает безопасные нажатия клавиш для печати локально даже при медленном соединении. Запросы паролей и управляющий ввод никогда не отображаются.", "localEchoDefault": "Использовать глобальные настройки", @@ -1072,8 +1069,6 @@ "bindHost": "Хост для привязки", "enableTmuxMonitor": "Включить монитор Tmux", "enableTmuxMonitorDesc": "Показывать этот хост в Мониторе Tmux и добавлять действия tmux в боковую панель", - "enableTerminalToolbar": "Включить панель инструментов терминала", - "enableTerminalToolbarDesc": "Отобразить панель быстрого доступа, закрепленную на активном терминале, для отображения инструментов и статистики этого хоста в режиме реального времени.", "connectionToolbar": "Панель инструментов подключения", "showConnectionToolbar": "Показать панель инструментов подключения", "showConnectionToolbarDesc": "Отображать панель инструментов в сеансах SSH, RDP, VNC и Telnet этого хоста. Также можно временно скрыть её с самой панели инструментов.", @@ -1550,9 +1545,7 @@ "links": "Ссылки", "navigate": "Перейти", "select": "Выбрать", - "toggleWith": "Переключить с помощью", - "localTerminal": "Локальный терминал", - "localTerminalDesc": "Откройте командную оболочку на этом компьютере." + "toggleWith": "Переключить с помощью" }, "splitScreen": { "paneEmpty": "Панель {{index}} пуста", @@ -1717,57 +1710,13 @@ "loadSharesFailed": "Не удалось загрузить активные акции." }, "terminal": { - "sessionExpiredNotice": "Ваша предыдущая сессия на {{host}} завершилась через {{minutes}} минут после разрыва соединения; оболочка и все запущенные в ней программы удалены. Включите Auto-Tmux на этом хосте, чтобы поддерживать сессии активными.", - "sessionExpiredNoticeNoMinutes": "Ваша предыдущая сессия на {{host}} завершилась во время отключения; оболочка и все запущенные в ней приложения удалены. Включите Auto-Tmux на этом хосте, чтобы поддерживать сессии активными.", - "enableAutoTmuxAction": "Включить Auto-Tmux", - "autoTmuxEnabled": "Автоматический Tmux включен для {{host}}. Он применяется со следующего соединения.", - "autoTmuxEnableFailed": "Не удалось включить Auto-Tmux.", "connect": "Подключиться к хосту", "clear": "Очистить", "paste": "Вставить", - "reconnect": "Переподключиться", - "connectionLost": "Соединение потеряно", "connected": "Подключено", "clipboardWriteFailed": "Не удалось скопировать в буфер обмена. Убедитесь, что страница загружена по HTTPS или с localhost.", "clipboardReadFailed": "Не удалось прочитать из буфера обмена. Убедитесь, что предоставлены разрешения на доступ к буферу обмена.", - "clipboardHttpWarning": "Не удалось прочитать содержимое буфера обмена. Некоторые браузеры ограничивают доступ к буферу обмена вне протокола HTTPS — попробуйте использовать Ctrl+V или используйте Termix через HTTPS.", - "passwordPromptFillTitle": "Вставить сохраненный пароль в этот запрос?", - "unknownError": "Произошла неизвестная ошибка", - "websocketError": "Ошибка подключения WebSocket", - "connecting": "Подключение...", - "noHostSelected": "Не выбран хост", - "reconnecting": "Переподключение... ({{attempt}}/{{max}})", - "reconnected": "Успешно переподключено", - "tmuxSessionCreated": "Сессия tmux создана: {{name}}", - "tmuxSessionAttached": "Сессия tmux подключена: {{name}}", - "tmuxUnavailable": "tmux не установлен на удалённом хосте, переход на стандартную оболочку", - "tmuxSessionPickerTitle": "Сессии tmux", - "tmuxSessionPickerDesc": "Найдены существующие сессии tmux на этом хосте. Выберите одну для переподключения или создайте новую сессию.", - "tmuxWindows": "Окна", - "tmuxWindowCount": "{{count}} окно", - "tmuxAttached": "Подключённые клиенты", - "tmuxAttachedCount": "{{count}} подключено", - "tmuxLastActivity": "Последняя активность", - "tmuxTimeJustNow": "только что", - "tmuxTimeMinutes": "{{count}}м назад", - "tmuxTimeHours": "{{count}}ч назад", - "tmuxTimeDays": "{{count}}д назад", - "tmuxCreateNew": "Начать новую сессию", - "tmuxCopyHint": "Скорректируйте выделение и нажмите Enter, чтобы скопировать в буфер обмена", "tmuxDetach": "Отключиться от сессии tmux", - "tmuxDetached": "Отключён от сессии tmux", - "searchPlaceholder": "Находить", - "searchCaseSensitive": "Спичечный коробок", - "searchWholeWord": "Сопоставьте целое слово", - "searchRegex": "Используйте регулярные выражения", - "searchNoResults": "Результаты отсутствуют", - "searchResultCount": "{{index}} из {{count}}", - "searchNext": "Следующий матч (Вход)", - "searchPrevious": "Предыдущий матч (Shift+Enter)", - "searchClose": "Закрыть (Выход)", - "maxReconnectAttemptsReached": "Достигнуто максимальное количество попыток переподключения", - "closeTab": "Закрыть", - "connectionTimeout": "Тайм-аут подключения", "terminalTitle": "Терминал - {{host}}", "terminalWithPath": "Терминал - {{host}}:{{path}}", "runTitle": "Выполнение {{command}} - {{host}}", @@ -1790,27 +1739,14 @@ "opksshOpenBrowser": "Открыть браузер для аутентификации", "opksshWaitingForAuth": "Ожидание аутентификации в браузере...", "opksshAuthenticating": "Обработка аутентификации...", - "opksshTimeout": "Время аутентификации истекло. Попробуйте снова.", - "opksshAuthFailed": "Аутентификация не удалась. Проверьте учётные данные и попробуйте снова.", "opksshSignInWith": "Войти через {{provider}}", - "vaultAuthTitle": "Требуется вход в Vault", - "vaultAuthDescription": "Открыто окно для входа в HashiCorp Vault. Завершите вход там; подключение продолжится автоматически.", - "vaultAuthFailed": "Аутентификация Vault не удалась. Попробуйте снова.", - "vaultReopen": "Повторно открыть окно входа", "sudoPasswordPopupTitle": "Вставить пароль?", - "linkDialogTitle": "Открыть ссылку", - "linkDialogOpen": "Открыть", - "linkDialogCopy": "Копировать", - "websocketAbnormalClose": "Соединение неожиданно закрыто. Возможно, проблема с обратным прокси или конфигурацией SSL. Проверьте журналы сервера.", "connectionLogTitle": "Журнал подключения", "connectionLogCopy": "Копировать журналы в буфер обмена", "connectionLogEmpty": "Журналы подключения пока отсутствуют", "connectionLogWaiting": "Ожидание журналов подключения...", "connectionLogCopied": "Журналы подключения скопированы в буфер обмена", "connectionLogCopyFailed": "Не удалось скопировать журналы в буфер обмена", - "connectionRejected": "Подключение отклонено сервером. Проверьте аутентификацию и сетевые настройки.", - "hostKeyRejected": "Проверка ключа хоста SSH отклонена. Подключение отменено.", - "sessionTakenOver": "Сеанс был открыт в другой вкладке. Переподключение...", "split": { "splitTab": "Разделить вкладку", "addToSplit": "Добавить в разделённую область", @@ -2225,55 +2161,6 @@ "sectionApiKeys": "API-ключи", "sectionAuditLog": "Журнал аудита", "sectionSsl": "SSL / Let's Encrypt", - "sectionTouchInput": "Сенсорный ввод", - "touchEnabled": "Включить сенсорный ввод", - "touchEnabledDesc": "Разрешить прокрутку одним пальцем в терминалах", - "touchMomentumEnabled": "Включить импульс", - "touchMomentumEnabledDesc": "После свайпа продолжите прокрутку еще немного.", - "touchAdvanced": "Передовой", - "touchDragThreshold": "Порог сопротивления", - "touchMaxWheelDelta": "Максимальная разница колес", - "touchMomentumSampleWindow": "Окно выборки импульса", - "touchReleaseGracePeriod": "Льготный период освобождения", - "touchMinimumVelocity": "Минимальная скорость", - "touchMaximumVelocity": "Максимальная скорость", - "touchMaximumDuration": "Максимальная продолжительность", - "touchMaximumTravel": "Максимальный ход", - "touchDecayTime": "Время распада", - "touchPixelsPerTick": "Пикселей на такт", - "touchMaximumFrameInterval": "Максимальный интервал кадров", - "touchMaximumTicksPerFrame": "Максимальное количество тактов на кадр", - "touchResetDefaults": "Сбросить до значений по умолчанию", - "touchSaved": "Сохранены настройки сенсорного ввода.", - "touchSaveFailed": "Не удалось сохранить настройки сенсорного ввода.", - "sectionImageStorage": "Хранилище изображений", - "imageStorageMode": "Режим хранения", - "imageStorageModeDesc": "Где записываются и загружаются образы терминала. Автоматический выбор по типу подключения, локальному и удаленному (SFTP) — это фиксированная настройка.", - "imageStorageModeAuto": "Авто", - "imageStorageModeLocal": "Местный", - "imageStorageModeRemoteSftp": "Удаленный (SFTP)", - "imageStorageLocalDir": "Локальный каталог", - "imageStorageLocalDirDesc": "В бэкэнде Termix отображается абсолютный путь. В целях безопасности он не отображается после сохранения.", - "imageStorageLocalDirPlaceholder": "Оставьте поле пустым для обновления.", - "imageStorageHostPath": "Путь к хосту", - "imageStorageHostPathDesc": "Представление локального каталога на стороне хоста, которое получают терминальные агенты.", - "imageStorageTtl": "TTL (мс)", - "imageStorageMaxCount": "Максимальное количество изображений", - "imageStorageMaxBytes": "Максимальный объем хранилища в байтах", - "imageStorageSaved": "Сохранены настройки хранения изображений.", - "imageStorageSaveFailed": "Не удалось сохранить настройки хранилища изображений.", - "imageStorageInstanceId": "Идентификатор экземпляра терминальной сессии", - "imageStorageInstanceIdDesc": "Идентификатор экземпляра одной из подключенных вкладок терминала, используемый для проверки видимости.", - "imageStorageInstanceIdRequired": "Сначала введите идентификатор экземпляра сеанса терминала.", - "imageStorageTest": "Тест", - "imageStorageTesting": "Тестирование...", - "imageStorageTestFailed": "Не удалось проверить хранилище изображений.", - "imageStorageTestConnected": "Терминальная сессия подключена.", - "imageStorageTestNotConnected": "Нет подключенной терминальной сессии с этим идентификатором экземпляра.", - "imageStorageTestSelectedMode": "Для загрузки будет использоваться следующий синтаксис: {{mode}}", - "imageStorageSelectedModeUnavailable": "Недоступно", - "imageStorageTestLocalVisible": "Локальный каталог виден с подключенного хоста.", - "imageStorageTestLocalNotVisible": "Локальный каталог НЕ виден с подключенного хоста.", "sectionBranding": "Белая этикетка", "brandingAppName": "Название приложения", "brandingAppNameDesc": "Отображается на экране входа в систему и на вкладке браузера. Оставьте поле пустым, чтобы использовать \"Termix\".", @@ -2354,9 +2241,6 @@ "oidcSilentLoginDefaultLockedDesc": "Этот параметр заблокирован переменной среды OIDC_SILENT_LOGIN_DEFAULT и не может быть изменен здесь.", "allowPasswordReset": "Разрешить сброс пароля", "allowPasswordResetDesc": "Код сброса в логах Docker", - "commandHistoryEnabled": "История команд", - "commandHistoryEnabledDesc": "Включить запись истории команд. При отключении история не сохраняется независимо от настроек хостов.", - "updateCommandHistoryFailed": "Не удалось обновить настройку истории команд", "analyticsEnabled": "Делитесь анонимной статистикой использования.", "analyticsEnabledDesc": "Ежедневно отправляет анонимный подсчет пользователей, хостов и использования функций, чтобы помочь улучшить Termix. Никакие личные данные или сведения о подключении никогда не включаются.", "analyticsEnabledLockedDesc": "Этот параметр заблокирован переменной среды ENABLE_TELEMETRY и не может быть изменен здесь.", @@ -2647,11 +2531,6 @@ "hostDefaultsTerminal": "Терминал", "hostDefaultsAutoTmux": "Auto-Tmux", "hostDefaultsAutoTmuxDesc": "Запускайте терминалы новых хостов внутри tmux, чтобы сессии сохранялись после разрывов соединения (для этого tmux должен быть установлен на хосте).", - "terminalSessionTimeout": "Сохранение сессии в терминале", - "terminalSessionTimeoutDesc": "Как долго отключенный терминал остается активным на сервере до его закрытия. Мин. 1 · Макс. 1440 минут. Для сессий, которые должны длиться дольше этого времени, включите Auto-Tmux на хосте.", - "terminalSessionTimeoutRange": "Введите значение от 1 до 1440 минут.", - "terminalSessionTimeoutSaved": "Сохранение данных о состоянии сессии в терминале", - "terminalSessionTimeoutSaveFailed": "Не удалось сохранить данные о состоянии терминальной сессии.", "minutes": "минут", "hostDefaultsSessionLogging": "Журналирование сеансов", "hostDefaultsSessionLoggingDesc": "По умолчанию записывать терминальные сеансы на новых хостах", @@ -2708,15 +2587,6 @@ "connectToTerminal": "Подключиться к терминалу", "connectToFiles": "Подключиться к файлам" }, - "history": { - "noTerminalSelected": "Терминал не выбран", - "noTerminalSelectedHint": "Откройте вкладку SSH-терминала, чтобы просмотреть историю команд", - "searchPlaceholder": "Поиск по истории...", - "clearAll": "Очистить всё", - "noHistoryEntries": "Нет записей в истории", - "trackingDisabled": "Отслеживание истории отключено", - "trackingDisabledHint": "Включите его в настройках терминала хоста." - }, "sshTools": { "keyRecordingTitle": "Запись клавиш", "recordToTerminals": "Записать в терминалы", @@ -3178,30 +3048,11 @@ "terminalToolbar": { "title": "Панель инструментов терминала", "showActions": "Показать панель инструментов", - "hideToolbar": "Скрыть панель инструментов", - "showToolbar": "Развернуть панель инструментов", - "moveToolbar": "Переместить панель инструментов", - "moveToolbarHint": "Перетащите панель инструментов, чтобы изменить ее положение.", - "layout": "режим отображения панели инструментов", - "layoutIcon": "Только иконка", - "layoutLabeled": "Иконки и подписи", - "layoutExpanded": "Дополнено метриками", - "image": "Действия с изображениями (только с изображениями)", - "upload": "Загрузить изображение", - "paste": "Вставить изображение", - "detachTmux": "Отсоединить tmux", - "detachTmuxDescription": "Отключить сессию tmux", - "imageActionFailed": "Операция с изображением не удалась.", - "retry": "Повторить попытку", - "dismissError": "Отклонить ошибку изображения", "cpu": "Процессор", "memory": "Память", "disk": "Диск", "statsUnavailable": "Статистика в режиме реального времени недоступна", "tmuxDetach": "Отключить сессию tmux", - "uploadImage": "Загрузите изображение на хостинг.", - "uploadingImage": "Загрузка...", - "pasteImage": "Вставить изображение из буфера обмена (только изображения)", "openFiles": "Открыть файлы", "openDocker": "Open Docker", "openTunnel": "Открытый туннель", @@ -3285,31 +3136,6 @@ "timeHours": "{{count}}ч назад", "timeDays": "{{count}}д назад" }, - "mobileKeyboard": { - "shift": "Shift", - "ctrl": "Ctrl", - "esc": "Esc", - "tab": "Tab", - "backTab": "⇥", - "arrowUp": "Вверх", - "arrowDown": "Вниз", - "arrowLeft": "Влево", - "arrowRight": "Вправо", - "home": "Home", - "end": "End", - "pageUp": "PgUp", - "pageDown": "PgDn", - "delete": "Del", - "paste": "Вставить", - "editQuickKeys": "Редактировать", - "quickKeysTitle": "Быстрые клавиши", - "quickKeysDesc": "Нажмите × для удаления. Поддерживается до 8 символов.", - "quickKeyPlaceholder": "напр. sudo ", - "addQuickKey": "Добавить", - "removeQuickKey": "Удалить", - "resetDefaults": "Сбросить по умолчанию", - "done": "Готово" - }, "serial": { "title": "Последовательный порт", "portLabel": "Порт", diff --git a/src/ui/locales/translated/sr_SP.json b/src/ui/locales/translated/sr_SP.json index 38d8138e4..1bc303157 100644 --- a/src/ui/locales/translated/sr_SP.json +++ b/src/ui/locales/translated/sr_SP.json @@ -742,7 +742,6 @@ "addHost": "Додај хоста", "editHost": "Измени хоста", "cloneHost": "Клонирај хост", - "enableTerminal": "Омогући терминал", "enableTunnel": "Омогући тунел", "enableFileManager": "Омогући менаџер датотека", "enableDocker": "Омогући Докер", @@ -1041,8 +1040,6 @@ "enableSessionLoggingDesc": "Снимите излаз терминалне сесије за каснији преглед", "allowSessionSharing": "Дозволи дељење сесије", "allowSessionSharingDesc": "Дозволите дељење сесија уживо на овом хосту путем линка или са другим корисницима", - "enableCommandHistory": "Историја команди", - "enableCommandHistoryDesc": "Снимање команди које се извршавају у овом терминалу за историју и аутоматско довршавање", "localEchoLabel": "Локални одјек", "localEchoDesc": "Предвиди безбедне притиске тастера за штампање локално на спорим везама. Упити за лозинку и контролни уноси се никада не понављају.", "localEchoDefault": "Користите глобална подешавања", @@ -1072,8 +1069,6 @@ "bindHost": "Повежи хост", "enableTmuxMonitor": "Омогући Tmux монитор", "enableTmuxMonitorDesc": "Прикажи овај хост у Tmux монитору и додај његове tmux акције у бочну траку", - "enableTerminalToolbar": "Омогући траку са алаткама терминала", - "enableTerminalToolbarDesc": "Прикажи траку са алаткама за брзи приступ причвршћену за активни терминал за алате и статистику уживо овог хоста", "connectionToolbar": "Трака са алаткама за повезивање", "showConnectionToolbar": "Прикажи траку са алаткама за повезивање", "showConnectionToolbarDesc": "Прикажи траку са алаткама у SSH, RDP, VNC и Telnet сесијама овог хоста. Такође је можете привремено сакрити са саме траке са алаткама.", @@ -1550,9 +1545,7 @@ "links": "Линкови", "navigate": "Навигација", "select": "Изаберите", - "toggleWith": "Укључи/искључи", - "localTerminal": "Локални терминал", - "localTerminalDesc": "Отворите командну шкољку на овом рачунару" + "toggleWith": "Укључи/искључи" }, "splitScreen": { "paneEmpty": "Окно {{index}} - празно", @@ -1717,57 +1710,13 @@ "loadSharesFailed": "Учитавање активних дељења није успело" }, "terminal": { - "sessionExpiredNotice": "Ваша претходна сесија на {{host}} је истекла након {{minutes}} минута од прекида везе; љуска и све што је у њој покренуто су нестали. Омогућите Auto-Tmux на овом хосту да би сесије остале активне.", - "sessionExpiredNoticeNoMinutes": "Ваша претходна сесија на {{host}} је истекла док сте били одвојени; љуска и све што је у њој покренуто су нестали. Омогућите Auto-Tmux на овом хосту да би сесије остале активне.", - "enableAutoTmuxAction": "Омогући аутоматски Tmux", - "autoTmuxEnabled": "Аутоматски Tmux је омогућен за {{host}}. Примењује се од следећег повезивања.", - "autoTmuxEnableFailed": "Није успело омогућавање аутоматског Tmux-а", "connect": "Повежи се са хостом", "clear": "Јасно", "paste": "Залепи", - "reconnect": "Поново се повежите", - "connectionLost": "Веза је прекинута", "connected": "Повезано", "clipboardWriteFailed": "Копирање у међуспремник није успело. Уверите се да се страница приказује преко HTTPS-а или localhost-а.", "clipboardReadFailed": "Читање из међуспремника није успело. Уверите се да су дозволе за међуспремник одобрене.", - "clipboardHttpWarning": "Није могуће прочитати међуспремник. Неки прегледачи ограничавају приступ међуспремнику ван HTTPS-а - покушајте са Ctrl+V или покрените Termix преко HTTPS-а.", - "passwordPromptFillTitle": "Унесите сачувану лозинку у овај промпт?", - "unknownError": "Дошло је до непознате грешке", - "websocketError": "Грешка у повезивању са WebSocket-ом", - "connecting": "Повезивање...", - "noHostSelected": "Није изабран домаћин", - "reconnecting": "Поновно повезивање... ({{attempt}}/{{max}})", - "reconnected": "Поново повезано успешно", - "tmuxSessionCreated": "tmux сесија креирана: {{name}}", - "tmuxSessionAttached": "приложена tmux сесија: {{name}}", - "tmuxUnavailable": "tmux није инсталиран на удаљеном хосту, враћа се на стандардну љуску", - "tmuxSessionPickerTitle": "tmux сесије", - "tmuxSessionPickerDesc": "Постојеће tmux сесије пронађене су на овом хосту. Изаберите једну да бисте је поново повезали или креирали нову сесију.", - "tmuxWindows": "Прозори", - "tmuxWindowCount": "{{count}} прозор", - "tmuxAttached": "Прикључени клијенти", - "tmuxAttachedCount": "{{count}} приложено", - "tmuxLastActivity": "Последња активност", - "tmuxTimeJustNow": "управо сада", - "tmuxTimeMinutes": "{{count}}пре мин", - "tmuxTimeHours": "{{count}}пре сати", - "tmuxTimeDays": "{{count}}пре д", - "tmuxCreateNew": "Започни нову сесију", - "tmuxCopyHint": "Прилагодите избор и притисните Ентер да бисте копирали у међуспремник", "tmuxDetach": "Одвоји се од tmux сесије", - "tmuxDetached": "Одвојено од tmux сесије", - "searchPlaceholder": "Пронађи", - "searchCaseSensitive": "Упаривање великог и малог дела", - "searchWholeWord": "Подударање целе речи", - "searchRegex": "Користите регуларни израз", - "searchNoResults": "Нема резултата", - "searchResultCount": "{{index}} од {{count}}", - "searchNext": "Следећи меч (Унесите)", - "searchPrevious": "Претходно подударање (Shift+Enter)", - "searchClose": "Затвори (Escape)", - "maxReconnectAttemptsReached": "Достигнут је максималан број покушаја поновног повезивања", - "closeTab": "Затвори", - "connectionTimeout": "Временско ограничење везе", "terminalTitle": "Терминал - {{host}}", "terminalWithPath": "Терминал - {{host}}:{{path}}", "runTitle": "Покреће се {{command}} - {{host}}", @@ -1790,27 +1739,14 @@ "opksshOpenBrowser": "Отворите прегледач за аутентификацију", "opksshWaitingForAuth": "Чекање аутентификације у прегледачу...", "opksshAuthenticating": "Обрада аутентификације...", - "opksshTimeout": "Временско ограничење за аутентификацију је истекло. Молимо покушајте поново.", - "opksshAuthFailed": "Аутентификација није успела. Проверите своје акредитиве и покушајте поново.", "opksshSignInWith": "Пријавите се са {{provider}}", - "vaultAuthTitle": "Потребно је пријављивање у трезор", - "vaultAuthDescription": "Отворио се прозор за пријављивање на HashiCorp Vault. Завршите пријављивање тамо; ова веза ће се аутоматски наставити.", - "vaultAuthFailed": "Аутентификација трезора није успела. Молимо покушајте поново.", - "vaultReopen": "Поново отвори прозор за пријављивање", "sudoPasswordPopupTitle": "Унети лозинку?", - "linkDialogTitle": "Отвори линк", - "linkDialogOpen": "Отворено", - "linkDialogCopy": "Копија", - "websocketAbnormalClose": "Веза је неочекивано прекинута. Ово може бити због проблема са обрнутим проксијем или SSL конфигурацијом. Молимо вас да проверите логове сервера.", "connectionLogTitle": "Дневник везе", "connectionLogCopy": "Копирајте логове у међуспремник", "connectionLogEmpty": "Још нема евиденција веза", "connectionLogWaiting": "Чекање евиденције везе...", "connectionLogCopied": "Дневници веза копирани у међуспремник", "connectionLogCopyFailed": "Копирање логова у међуспремник није успело", - "connectionRejected": "Сервер је одбио везу. Проверите аутентификацију и конфигурацију мреже.", - "hostKeyRejected": "Верификација SSH кључа хоста је одбијена. Веза је отказана.", - "sessionTakenOver": "Сесија је отворена у другој картици. Поновно повезивање...", "split": { "splitTab": "Подели картицу", "addToSplit": "Додај у Сплит", @@ -2225,55 +2161,6 @@ "sectionApiKeys": "API кључеви", "sectionAuditLog": "Дневник ревизије", "sectionSsl": "SSL / Хајде да шифрујемо", - "sectionTouchInput": "Унос додиром", - "touchEnabled": "Омогући унос додиром", - "touchEnabledDesc": "Дозволите скроловање једним прстом у терминалима", - "touchMomentumEnabled": "Омогући Моментум", - "touchMomentumEnabledDesc": "Наставите кратко да скролујете након превлачења прстом", - "touchAdvanced": "Напредно", - "touchDragThreshold": "Праг превлачења", - "touchMaxWheelDelta": "Максимална делта точка", - "touchMomentumSampleWindow": "Прозор узорка момента", - "touchReleaseGracePeriod": "Грејс период за објављивање", - "touchMinimumVelocity": "Минимална брзина", - "touchMaximumVelocity": "Максимална брзина", - "touchMaximumDuration": "Максимално трајање", - "touchMaximumTravel": "Максимални ход", - "touchDecayTime": "Време распада", - "touchPixelsPerTick": "Пиксела по тик-у", - "touchMaximumFrameInterval": "Максимални интервал кадра", - "touchMaximumTicksPerFrame": "Максималан број откуцаја по кадру", - "touchResetDefaults": "Ресетуј на подразумеване вредности", - "touchSaved": "Подешавања уноса додиром су сачувана", - "touchSaveFailed": "Чување подешавања за унос додиром није успело", - "sectionImageStorage": "Складиштење слика", - "imageStorageMode": "Режим складиштења", - "imageStorageModeDesc": "Где се записују налепљене и отпремљене слике терминала. Аутоматски избори по конекцији, локални и удаљени (SFTP) су поправљени.", - "imageStorageModeAuto": "Аутоматски", - "imageStorageModeLocal": "Локално", - "imageStorageModeRemoteSftp": "Удаљено (SFTP)", - "imageStorageLocalDir": "Локални директоријум", - "imageStorageLocalDirDesc": "Апсолутна путања на серверској страни Termix-а. Не приказује се након чувања из безбедносних разлога.", - "imageStorageLocalDirPlaceholder": "Оставите празно да бисте остали ажурни", - "imageStorageHostPath": "Путања хоста", - "imageStorageHostPathDesc": "Поглед са стране хоста на локални директоријум који терминални агенти примају.", - "imageStorageTtl": "TTL (ms)", - "imageStorageMaxCount": "Максималан број слика", - "imageStorageMaxBytes": "Максимални број бајтова за складиштење", - "imageStorageSaved": "Подешавања складиштења слика сачувана", - "imageStorageSaveFailed": "Чување подешавања за складиштење слика није успело", - "imageStorageInstanceId": "ИД инстанце терминалне сесије", - "imageStorageInstanceIdDesc": "ИД инстанце једне од ваших повезаних картица терминала, која се користи за тестирање видљивости.", - "imageStorageInstanceIdRequired": "Прво унесите ИД инстанце терминалне сесије", - "imageStorageTest": "Тест", - "imageStorageTesting": "Тестирање...", - "imageStorageTestFailed": "Тестирање складиштења слика није успело", - "imageStorageTestConnected": "Терминална сесија је повезана.", - "imageStorageTestNotConnected": "Нема повезане терминалне сесије са овим ID-ом инстанце.", - "imageStorageTestSelectedMode": "Отпремање би користило: {{mode}}", - "imageStorageSelectedModeUnavailable": "Недоступно", - "imageStorageTestLocalVisible": "Локални директоријум је видљив са повезаног хоста.", - "imageStorageTestLocalNotVisible": "Локални директоријум НИЈЕ видљив са повезаног хоста.", "sectionBranding": "Бела етикета", "brandingAppName": "Назив апликације", "brandingAppNameDesc": "Приказује се на екрану за пријаву и картици прегледача. Оставите празно да бисте користили „Termix“.", @@ -2354,9 +2241,6 @@ "oidcSilentLoginDefaultLockedDesc": "Ово подешавање је закључано променљивом окружења OIDC_SILENT_LOGIN_DEFAULT и не може се овде променити.", "allowPasswordReset": "Дозволи ресетовање лозинке", "allowPasswordResetDesc": "Ресетујте код путем Докер логова", - "commandHistoryEnabled": "Историја команди", - "commandHistoryEnabledDesc": "Дозволи снимање историје команди. Када је онемогућено, историја се не чува без обзира на подешавања за сваки хост.", - "updateCommandHistoryFailed": "Ажурирање подешавања историје команди није успело", "analyticsEnabled": "Делите анонимну статистику коришћења", "analyticsEnabledDesc": "Шаље анонимни дневни број корисника, хостова и коришћења функција како би се побољшао Termix. Никада се не укључују лични подаци или детаљи везе.", "analyticsEnabledLockedDesc": "Ово подешавање је закључано променљивом окружења ENABLE_TELEMETRY и не може се овде променити.", @@ -2647,11 +2531,6 @@ "hostDefaultsTerminal": "Терминал", "hostDefaultsAutoTmux": "Ауто-Тмукс", "hostDefaultsAutoTmuxDesc": "Покрећи терминале нових хостова унутар tmux-а како би сесије преживеле прекиде везе (потребан је tmux на хосту)", - "terminalSessionTimeout": "Трајност терминалне сесије", - "terminalSessionTimeoutDesc": "Колико дуго се искључени терминал одржава активним на серверу пре него што се затвори. Мин. 1 · Макс. 1440 минута. За сесије које морају да трају дуже од овог периода, омогућите Auto-Tmux на хосту.", - "terminalSessionTimeoutRange": "Унесите вредност између 1 и 1440 минута", - "terminalSessionTimeoutSaved": "Трајност терминалне сесије је сачувана", - "terminalSessionTimeoutSaveFailed": "Није успело чување трајности сесије терминала", "minutes": "минута", "hostDefaultsSessionLogging": "Записивање сесије", "hostDefaultsSessionLoggingDesc": "Подразумевано снимање терминалних сесија на новим хостовима", @@ -2708,15 +2587,6 @@ "connectToTerminal": "Повежи се са терминалом", "connectToFiles": "Повежи се са датотекама" }, - "history": { - "noTerminalSelected": "Није изабран терминал", - "noTerminalSelectedHint": "Отворите картицу SSH терминала да бисте видели историју његових команди", - "searchPlaceholder": "Историја претраге...", - "clearAll": "Обриши све", - "noHistoryEntries": "Нема уноса у историји", - "trackingDisabled": "Праћење историје је онемогућено", - "trackingDisabledHint": "Омогућите то у подешавањима терминала хоста." - }, "sshTools": { "keyRecordingTitle": "Снимање кључа", "recordToTerminals": "Снимање на терминале", @@ -3178,30 +3048,11 @@ "terminalToolbar": { "title": "Трака са алаткама терминала", "showActions": "Прикажи траку са алаткама", - "hideToolbar": "Сакриј траку са алаткама", - "showToolbar": "Прошири траку са алаткама", - "moveToolbar": "Помери траку са алаткама.", - "moveToolbarHint": "Превуците да бисте променили положај траке са алаткама", - "layout": "Режим приказа алатне траке", - "layoutIcon": "Само икона", - "layoutLabeled": "Иконе и ознаке", - "layoutExpanded": "Проширено са метрикама", - "image": "Радње са сликама (само слике)", - "upload": "Отпреми слику", - "paste": "Налепи слику", - "detachTmux": "Одвојите tmux", - "detachTmuxDescription": "Одвојите tmux сесију", - "imageActionFailed": "Акција са сликом није успела", - "retry": "Покушај поново", - "dismissError": "Грешка при одбацивању слике", "cpu": "Процесор", "memory": "Памћење", "disk": "Диск", "statsUnavailable": "Статистика уживо није доступна", "tmuxDetach": "Одвојите tmux сесију", - "uploadImage": "Отпремите слику на хост", - "uploadingImage": "Отпремање...", - "pasteImage": "Налепи слику из међуспремника (само слике)", "openFiles": "Отвори датотеке", "openDocker": "Отвори Докер", "openTunnel": "Отворени тунел", @@ -3285,31 +3136,6 @@ "timeHours": "{{count}}пре сати", "timeDays": "{{count}}пре д" }, - "mobileKeyboard": { - "shift": "Померање", - "ctrl": "Ctrl", - "esc": "Излаз", - "tab": "Таб", - "backTab": "⇥", - "arrowUp": "Стрелица нагоре", - "arrowDown": "Стрелица надоле", - "arrowLeft": "Стрелица лево", - "arrowRight": "Стрелица десно", - "home": "Дом", - "end": "Крај", - "pageUp": "PgUp", - "pageDown": "ПгДн", - "delete": "Дел", - "paste": "Залепи", - "editQuickKeys": "Уреди брзе тастере", - "quickKeysTitle": "Брзи тастери", - "quickKeysDesc": "Додирните × да бисте уклонили. Подржава до 8 знакова.", - "quickKeyPlaceholder": "нпр. sudo ", - "addQuickKey": "Додај", - "removeQuickKey": "Уклони", - "resetDefaults": "Ресетуј на подразумеване вредности", - "done": "Готово" - }, "serial": { "title": "Серијски", "portLabel": "Лука", diff --git a/src/ui/locales/translated/sv_SE.json b/src/ui/locales/translated/sv_SE.json index 693f53d11..212556cbf 100644 --- a/src/ui/locales/translated/sv_SE.json +++ b/src/ui/locales/translated/sv_SE.json @@ -742,7 +742,6 @@ "addHost": "Lägg till värd", "editHost": "Redigera värd", "cloneHost": "Klona värd", - "enableTerminal": "Aktivera terminalen", "enableTunnel": "Aktivera tunnel", "enableFileManager": "Aktivera filhanteraren", "enableDocker": "Aktivera Docker", @@ -1041,8 +1040,6 @@ "enableSessionLoggingDesc": "Spela in utdata från terminalsessionen för senare granskning", "allowSessionSharing": "Tillåt sessionsdelning", "allowSessionSharingDesc": "Tillåt att livesessioner på den här värden delas via länk eller med andra användare", - "enableCommandHistory": "Kommandohistorik", - "enableCommandHistoryDesc": "Record-kommandon körs i den här terminalen för historik och autofullständig ifyllning", "localEchoLabel": "Lokalt eko", "localEchoDesc": "Förutsäg säkra utskrivbara tangenttryckningar lokalt på långsamma anslutningar. Lösenordsförfrågningar och kontrollinmatning upprepas aldrig.", "localEchoDefault": "Använd global inställning", @@ -1072,8 +1069,6 @@ "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", - "enableTerminalToolbar": "Aktivera terminalverktygsfältet", - "enableTerminalToolbarDesc": "Visa en snabbåtkomstverktygsfält dockad till den aktiva terminalen för den här värdens verktyg och livestatistik", "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.", @@ -1550,9 +1545,7 @@ "links": "Länkar", "navigate": "Navigera", "select": "Välja", - "toggleWith": "Växla med", - "localTerminal": "Lokal terminal", - "localTerminalDesc": "Öppna ett shell på den här datorn" + "toggleWith": "Växla med" }, "splitScreen": { "paneEmpty": "Ruta {{index}} - tom", @@ -1717,57 +1710,13 @@ "loadSharesFailed": "Misslyckades med att läsa in aktiva delningar" }, "terminal": { - "sessionExpiredNotice": "Din föregående session på {{host}} gick ut efter {{minutes}} minuters frånkoppling; skalet och allt som körs i det är borta. Aktivera Auto-Tmux på den här värden för att hålla sessionerna aktiva.", - "sessionExpiredNoticeNoMinutes": "Din föregående session på {{host}} upphörde medan den var frånkopplad; skalet och allt som körs i det är borta. Aktivera Auto-Tmux på den här värden för att hålla sessionerna aktiva.", - "enableAutoTmuxAction": "Aktivera Auto-Tmux", - "autoTmuxEnabled": "Auto-Tmux aktiverat för {{host}}. Det gäller från nästa anslutning.", - "autoTmuxEnableFailed": "Misslyckades med att aktivera Auto-Tmux", "connect": "Anslut till värd", "clear": "Rensa", "paste": "Klistra", - "reconnect": "Återanslut", - "connectionLost": "Anslutningen förlorades", "connected": "Ansluten", "clipboardWriteFailed": "Kunde inte kopiera till urklipp. Kontrollera att sidan serveras via HTTPS eller localhost.", "clipboardReadFailed": "Kunde inte läsa från urklipp. Kontrollera att behörigheter för urklipp är beviljade.", - "clipboardHttpWarning": "Kunde inte läsa urklippet. Vissa webbläsare begränsar åtkomst till urklipp utanför HTTPS - prova Ctrl+V istället, eller servera Termix över HTTPS.", - "passwordPromptFillTitle": "Fyll i det sparade lösenordet i den här frågan?", - "unknownError": "Okänt fel inträffade", - "websocketError": "WebSocket-anslutningsfel", - "connecting": "Ansluter...", - "noHostSelected": "Ingen värd vald", - "reconnecting": "Återansluter... ({{attempt}}/{{max}})", - "reconnected": "Återansluten lyckades", - "tmuxSessionCreated": "tmux-session skapad: {{name}}", - "tmuxSessionAttached": "tmux-session bifogad: {{name}}", - "tmuxUnavailable": "tmux är inte installerat på fjärrdatorn, utan återgår till standardskal", - "tmuxSessionPickerTitle": "tmux-sessioner", - "tmuxSessionPickerDesc": "Befintliga tmux-sessioner hittades på den här värden. Välj en för att återansluta eller skapa en ny session.", - "tmuxWindows": "Fönster", - "tmuxWindowCount": "{{count}} fönster", - "tmuxAttached": "Anslutna klienter", - "tmuxAttachedCount": "{{count}} bifogad", - "tmuxLastActivity": "Senaste aktivitet", - "tmuxTimeJustNow": "just nu", - "tmuxTimeMinutes": "för {{count}}minuter sedan", - "tmuxTimeHours": "för {{count}}timmar sedan", - "tmuxTimeDays": "för {{count}}dagar sedan", - "tmuxCreateNew": "Starta ny session", - "tmuxCopyHint": "Justera markeringen och tryck på Enter för att kopiera till urklipp", "tmuxDetach": "Koppla bort från tmux-sessionen", - "tmuxDetached": "Losskopplad från tmux-sessionen", - "searchPlaceholder": "Hitta", - "searchCaseSensitive": "Matchfodral", - "searchWholeWord": "Matcha hela ordet", - "searchRegex": "Använd reguljärt uttryck", - "searchNoResults": "Inga resultat", - "searchResultCount": "{{index}} av {{count}}", - "searchNext": "Nästa match (Enter)", - "searchPrevious": "Föregående matchning (Shift+Enter)", - "searchClose": "Stäng (Escape)", - "maxReconnectAttemptsReached": "Maximalt antal återanslutningsförsök uppnådda", - "closeTab": "Nära", - "connectionTimeout": "Anslutningstimeout", "terminalTitle": "Terminal - {{host}}", "terminalWithPath": "Terminal - {{host}}:{{path}}", "runTitle": "Kör {{command}} - {{host}}", @@ -1790,27 +1739,14 @@ "opksshOpenBrowser": "Öppna webbläsaren för att autentisera", "opksshWaitingForAuth": "Väntar på autentisering i webbläsaren...", "opksshAuthenticating": "Bearbetar autentisering...", - "opksshTimeout": "Autentiseringen har gått ut. Försök igen.", - "opksshAuthFailed": "Autentiseringen misslyckades. Kontrollera dina inloggningsuppgifter och försök igen.", "opksshSignInWith": "Logga in med {{provider}}", - "vaultAuthTitle": "Inloggning till arkivet krävs", - "vaultAuthDescription": "Ett fönster har öppnats för att logga in på HashiCorp Vault. Slutför inloggningen där; anslutningen fortsätter automatiskt.", - "vaultAuthFailed": "Valvautentisering misslyckades. Försök igen.", - "vaultReopen": "Öppna inloggningsfönstret igen", "sudoPasswordPopupTitle": "Ange lösenord?", - "linkDialogTitle": "Öppna länk", - "linkDialogOpen": "Öppna", - "linkDialogCopy": "Kopiera", - "websocketAbnormalClose": "Anslutningen stängdes oväntat. Detta kan bero på ett problem med omvänd proxy eller SSL-konfiguration. Kontrollera serverloggarna.", "connectionLogTitle": "Anslutningslogg", "connectionLogCopy": "Kopiera loggar till urklipp", "connectionLogEmpty": "Inga anslutningsloggar ännu", "connectionLogWaiting": "Väntar på anslutningsloggar...", "connectionLogCopied": "Anslutningsloggar kopierade till urklipp", "connectionLogCopyFailed": "Misslyckades med att kopiera loggar till urklipp", - "connectionRejected": "Anslutningen avvisades av servern. Kontrollera din autentisering och nätverkskonfiguration.", - "hostKeyRejected": "Verifiering av SSH-värdnyckel avvisad. Anslutningen avbröts.", - "sessionTakenOver": "Sessionen öppnades i en annan flik. Återansluter...", "split": { "splitTab": "Dela fliken", "addToSplit": "Lägg till i delning", @@ -2225,55 +2161,6 @@ "sectionApiKeys": "API-nycklar", "sectionAuditLog": "Granskningslogg", "sectionSsl": "SSL / Låt oss kryptera", - "sectionTouchInput": "Pekinmatning", - "touchEnabled": "Aktivera pekinmatning", - "touchEnabledDesc": "Tillåt scrollning med ett finger i terminaler", - "touchMomentumEnabled": "Aktivera momentum", - "touchMomentumEnabledDesc": "Fortsätt skrolla kort efter en svepning", - "touchAdvanced": "Avancerad", - "touchDragThreshold": "Draggräns", - "touchMaxWheelDelta": "Maximal hjuldelta", - "touchMomentumSampleWindow": "Momentum-exempelfönster", - "touchReleaseGracePeriod": "Frigivningsfrist", - "touchMinimumVelocity": "Minsta hastighet", - "touchMaximumVelocity": "Maximal hastighet", - "touchMaximumDuration": "Maximal varaktighet", - "touchMaximumTravel": "Maximal resa", - "touchDecayTime": "Avklingningstid", - "touchPixelsPerTick": "Pixlar per tick", - "touchMaximumFrameInterval": "Maximalt bildintervall", - "touchMaximumTicksPerFrame": "Maximalt antal ticks per bildruta", - "touchResetDefaults": "Återställ till standardinställningarna", - "touchSaved": "Inställningar för pekinmatning har sparats", - "touchSaveFailed": "Det gick inte att spara inställningarna för pekinmatning", - "sectionImageStorage": "Bildlagring", - "imageStorageMode": "Lagringsläge", - "imageStorageModeDesc": "Där inklistrade och uppladdade terminalbilder skrivs. Automatiska val via anslutning, lokal och fjärransluten (SFTP) är åtgärdade.", - "imageStorageModeAuto": "Bil", - "imageStorageModeLocal": "Lokal", - "imageStorageModeRemoteSftp": "Fjärrstyrning (SFTP)", - "imageStorageLocalDir": "Lokal katalog", - "imageStorageLocalDirDesc": "Absolut sökväg på Termix-backend. Visas inte efter sparning av säkerhetsskäl.", - "imageStorageLocalDirPlaceholder": "Lämna tomt för att hålla dig uppdaterad", - "imageStorageHostPath": "Värdsökväg", - "imageStorageHostPathDesc": "Värdsidans vy av den lokala katalogen som terminalagenter tar emot.", - "imageStorageTtl": "TTL (ms)", - "imageStorageMaxCount": "Max antal bilder", - "imageStorageMaxBytes": "Max antal lagringsbyte", - "imageStorageSaved": "Inställningar för bildlagring sparade", - "imageStorageSaveFailed": "Misslyckades med att spara inställningarna för bildlagring", - "imageStorageInstanceId": "Terminalsessionsinstans-ID", - "imageStorageInstanceIdDesc": "Instans-ID för en av dina anslutna terminalflikar, används för att testa synligheten.", - "imageStorageInstanceIdRequired": "Ange först ett terminalsessionsinstans-ID", - "imageStorageTest": "Testa", - "imageStorageTesting": "Testning...", - "imageStorageTestFailed": "Misslyckades med att testa bildlagring", - "imageStorageTestConnected": "Terminalsessionen är ansluten.", - "imageStorageTestNotConnected": "Ingen ansluten terminalsession med detta instans-ID.", - "imageStorageTestSelectedMode": "En uppladdning skulle använda: {{mode}}", - "imageStorageSelectedModeUnavailable": "Inte tillgänglig", - "imageStorageTestLocalVisible": "Den lokala katalogen är synlig från den anslutna värden.", - "imageStorageTestLocalNotVisible": "Den lokala katalogen är INTE synlig från den anslutna värden.", "sectionBranding": "Vit etikett", "brandingAppName": "Applikationsnamn", "brandingAppNameDesc": "Visas på inloggningsskärmen och i webbläsarfliken. Lämna tomt för att använda \"Termix\".", @@ -2354,9 +2241,6 @@ "oidcSilentLoginDefaultLockedDesc": "Den här inställningen är låst av miljövariabeln OIDC_SILENT_LOGIN_DEFAULT och kan inte ändras här.", "allowPasswordReset": "Tillåt lösenordsåterställning", "allowPasswordResetDesc": "Återställ kod via Docker-loggar", - "commandHistoryEnabled": "Kommandohistorik", - "commandHistoryEnabledDesc": "Tillåt inspelning av kommandohistorik. När det är inaktiverat sparas inte historiken oavsett inställningarna per värd.", - "updateCommandHistoryFailed": "Misslyckades med att uppdatera inställningen för kommandohistorik", "analyticsEnabled": "Dela anonym användningsstatistik", "analyticsEnabledDesc": "Skickar en anonym daglig räkning av användare, värdar och funktionsanvändning för att förbättra Termix. Inga personuppgifter eller anslutningsdetaljer inkluderas någonsin.", "analyticsEnabledLockedDesc": "Den här inställningen är låst av miljövariabeln ENABLE_TELEMETRY och kan inte ändras här.", @@ -2647,11 +2531,6 @@ "hostDefaultsTerminal": "Terminal", "hostDefaultsAutoTmux": "Auto-Tmux", "hostDefaultsAutoTmuxDesc": "Kör nya värdars terminaler inuti tmux så att sessioner överlever frånkopplingar (kräver tmux på värden)", - "terminalSessionTimeout": "Terminalsessionens persistens", - "terminalSessionTimeoutDesc": "Hur länge en frånkopplad terminal hålls aktiv på servern innan den stängs. Min 1 · Max 1440 minuter. För sessioner som måste överleva detta, aktivera Auto-Tmux på värden.", - "terminalSessionTimeoutRange": "Ange ett värde mellan 1 och 1440 minuter", - "terminalSessionTimeoutSaved": "Terminalsessionens varaktighet sparad", - "terminalSessionTimeoutSaveFailed": "Misslyckades med att spara terminalsessionens persistens", "minutes": "minuter", "hostDefaultsSessionLogging": "Sessionsloggning", "hostDefaultsSessionLoggingDesc": "Spela in terminalsessioner på nya värdar som standard", @@ -2708,15 +2587,6 @@ "connectToTerminal": "Anslut till terminalen", "connectToFiles": "Anslut till filer" }, - "history": { - "noTerminalSelected": "Ingen terminal vald", - "noTerminalSelectedHint": "Öppna en SSH-terminalflik för att visa dess kommandohistorik", - "searchPlaceholder": "Sökhistorik...", - "clearAll": "Rensa alla", - "noHistoryEntries": "Inga historikposter", - "trackingDisabled": "Historikspårning är inaktiverad", - "trackingDisabledHint": "Aktivera det i värdens terminalinställningar." - }, "sshTools": { "keyRecordingTitle": "Nyckelinspelning", "recordToTerminals": "Spela in till terminaler", @@ -3178,30 +3048,11 @@ "terminalToolbar": { "title": "Terminalverktygsfält", "showActions": "Visa verktygsfält", - "hideToolbar": "Dölj verktygsfältet", - "showToolbar": "Expandera verktygsfältet", - "moveToolbar": "Flytta verktygsfältet", - "moveToolbarHint": "Dra för att flytta verktygsfältet", - "layout": "Verktygsfältets visningsläge", - "layoutIcon": "Endast ikon", - "layoutLabeled": "Ikoner och etiketter", - "layoutExpanded": "Utökad med mätvärden", - "image": "Bildåtgärder (endast bilder)", - "upload": "Ladda upp bild", - "paste": "Klistra in bild", - "detachTmux": "Lossa tmux", - "detachTmuxDescription": "Koppla bort tmux-session", - "imageActionFailed": "Bildåtgärden misslyckades", - "retry": "Försöka igen", - "dismissError": "Stäng bildfel", "cpu": "CPU", "memory": "Minne", "disk": "Disk", "statsUnavailable": "Livestatistik är inte tillgänglig", "tmuxDetach": "Koppla bort tmux-session", - "uploadImage": "Ladda upp en bild till värden", - "uploadingImage": "Laddar upp...", - "pasteImage": "Klistra in bild från urklipp (endast bilder)", "openFiles": "Öppna filer", "openDocker": "Öppna Docker", "openTunnel": "Öppen tunnel", @@ -3285,31 +3136,6 @@ "timeHours": "för {{count}}timmar sedan", "timeDays": "för {{count}}dagar sedan" }, - "mobileKeyboard": { - "shift": "Flytta", - "ctrl": "Ctrl", - "esc": "Esc", - "tab": "Flik", - "backTab": "⇥", - "arrowUp": "Pil upp", - "arrowDown": "Pil nedåt", - "arrowLeft": "Pil vänster", - "arrowRight": "Pil höger", - "home": "Hem", - "end": "Avsluta", - "pageUp": "PgUp", - "pageDown": "PgDn", - "delete": "Del", - "paste": "Klistra", - "editQuickKeys": "Redigera snabbknappar", - "quickKeysTitle": "Snabbtangenter", - "quickKeysDesc": "Tryck på × för att ta bort. Stöder upp till 8 tecken.", - "quickKeyPlaceholder": "t.ex. sudo ", - "addQuickKey": "Tillägga", - "removeQuickKey": "Ta bort", - "resetDefaults": "Återställ till standardinställningarna", - "done": "Gjort" - }, "serial": { "title": "Serie", "portLabel": "Hamn", diff --git a/src/ui/locales/translated/th_TH.json b/src/ui/locales/translated/th_TH.json index 2ba659dc7..b5e95a04f 100644 --- a/src/ui/locales/translated/th_TH.json +++ b/src/ui/locales/translated/th_TH.json @@ -742,7 +742,6 @@ "addHost": "เพิ่มโฮสต์", "editHost": "แก้ไขโฮสต์", "cloneHost": "โคลนโฮสต์", - "enableTerminal": "เปิดใช้งานเทอร์มินัล", "enableTunnel": "เปิดใช้งานอุโมงค์", "enableFileManager": "เปิดใช้งานตัวจัดการไฟล์", "enableDocker": "เปิดใช้งาน Docker", @@ -1041,8 +1040,6 @@ "enableSessionLoggingDesc": "บันทึกผลลัพธ์จากเทอร์มินัลเพื่อตรวจสอบในภายหลัง", "allowSessionSharing": "อนุญาตการแชร์เซสชัน", "allowSessionSharingDesc": "อนุญาตให้แชร์เซสชันสดบนโฮสต์นี้ผ่านลิงก์หรือกับผู้ใช้รายอื่น", - "enableCommandHistory": "ประวัติคำสั่ง", - "enableCommandHistoryDesc": "บันทึกคำสั่งที่เรียกใช้ในเทอร์มินัลนี้เพื่อดูประวัติและฟังก์ชันเติมคำอัตโนมัติ", "localEchoLabel": "เสียงสะท้อนในท้องถิ่น", "localEchoDesc": "คาดการณ์การกดแป้นพิมพ์ที่ปลอดภัยสำหรับการพิมพ์ในเครื่องบนการเชื่อมต่อที่ช้า ข้อความแจ้งให้ป้อนรหัสผ่านและการป้อนข้อมูลควบคุมจะไม่ถูกแสดงซ้ำ", "localEchoDefault": "ใช้การตั้งค่าทั่วโลก", @@ -1072,8 +1069,6 @@ "bindHost": "โฮสต์ผูก", "enableTmuxMonitor": "เปิดใช้งาน Tmux Monitor", "enableTmuxMonitorDesc": "แสดงโฮสต์นี้ใน Tmux Monitor และเพิ่มการดำเนินการ tmux ของโฮสต์นี้ลงในแถบด้านข้าง", - "enableTerminalToolbar": "เปิดใช้งานแถบเครื่องมือเทอร์มินัล", - "enableTerminalToolbarDesc": "แสดงแถบเครื่องมือเข้าถึงด่วนที่ตรึงอยู่กับเทอร์มินัลที่ใช้งานอยู่ สำหรับเครื่องมือและสถิติแบบเรียลไทม์ของโฮสต์นี้", "connectionToolbar": "แถบเครื่องมือการเชื่อมต่อ", "showConnectionToolbar": "แสดงแถบเครื่องมือการเชื่อมต่อ", "showConnectionToolbarDesc": "แสดงแถบเครื่องมือในเซสชัน SSH, RDP, VNC และ Telnet ของโฮสต์นี้ คุณยังสามารถซ่อนแถบเครื่องมือชั่วคราวได้จากแถบเครื่องมือโดยตรง", @@ -1550,9 +1545,7 @@ "links": "ลิงก์", "navigate": "นำทาง", "select": "เลือก", - "toggleWith": "สลับด้วย", - "localTerminal": "สถานีปลายทางท้องถิ่น", - "localTerminalDesc": "เปิดเชลล์บนคอมพิวเตอร์เครื่องนี้" + "toggleWith": "สลับด้วย" }, "splitScreen": { "paneEmpty": "บานหน้าต่าง {{index}} - ว่างเปล่า", @@ -1717,57 +1710,13 @@ "loadSharesFailed": "ไม่สามารถโหลดหุ้นที่ใช้งานอยู่ได้" }, "terminal": { - "sessionExpiredNotice": "เซสชันก่อนหน้าของคุณบน {{host}} หมดอายุหลังจาก {{minutes}} นาทีถูกตัดการเชื่อมต่อ เชลล์และทุกสิ่งที่กำลังทำงานอยู่ภายในนั้นหายไปแล้ว โปรดเปิดใช้งาน Auto-Tmux บนโฮสต์นี้เพื่อรักษาสถานะเซสชันให้คงอยู่", - "sessionExpiredNoticeNoMinutes": "เซสชันก่อนหน้าของคุณบน {{host}} หมดอายุขณะที่ไม่ได้เชื่อมต่อ เชลล์และทุกอย่างที่กำลังทำงานอยู่ภายในนั้นหายไปแล้ว โปรดเปิดใช้งาน Auto-Tmux บนโฮสต์นี้เพื่อรักษาสถานะเซสชันให้คงอยู่", - "enableAutoTmuxAction": "เปิดใช้งาน Auto-Tmux", - "autoTmuxEnabled": "เปิดใช้งาน Auto-Tmux สำหรับ {{host}}แล้ว จะมีผลตั้งแต่การเชื่อมต่อครั้งถัดไป", - "autoTmuxEnableFailed": "ไม่สามารถเปิดใช้งาน Auto-Tmux ได้", "connect": "เชื่อมต่อกับโฮสต์", "clear": "ชัดเจน", "paste": "แปะ", - "reconnect": "เชื่อมต่อใหม่", - "connectionLost": "การเชื่อมต่อขาดหาย", "connected": "เชื่อมต่อแล้ว", "clipboardWriteFailed": "ไม่สามารถคัดลอกไปยังคลิปบอร์ดได้ โปรดตรวจสอบให้แน่ใจว่าหน้าเว็บนั้นให้บริการผ่าน HTTPS หรือ localhost", "clipboardReadFailed": "ไม่สามารถอ่านข้อมูลจากคลิปบอร์ดได้ โปรดตรวจสอบให้แน่ใจว่าได้ให้สิทธิ์การเข้าถึงคลิปบอร์ดแล้ว", - "clipboardHttpWarning": "ไม่สามารถอ่านข้อมูลในคลิปบอร์ดได้ เบราว์เซอร์บางตัวจำกัดการเข้าถึงคลิปบอร์ดนอก HTTPS - ลองใช้ Ctrl+V แทน หรือใช้งาน Termix ผ่าน HTTPS", - "passwordPromptFillTitle": "กรอกรหัสผ่านที่บันทึกไว้ลงในช่องนี้ใช่หรือไม่?", - "unknownError": "เกิดข้อผิดพลาดที่ไม่ทราบสาเหตุ", - "websocketError": "ข้อผิดพลาดในการเชื่อมต่อ WebSocket", - "connecting": "กำลังเชื่อมต่อ...", - "noHostSelected": "ไม่ได้เลือกโฮสต์", - "reconnecting": "กำลังเชื่อมต่อใหม่... ({{attempt}}/{{max}})", - "reconnected": "เชื่อมต่อสำเร็จแล้ว", - "tmuxSessionCreated": "สร้างเซสชัน tmux แล้ว: {{name}}", - "tmuxSessionAttached": "การเชื่อมต่อเซสชัน tmux: {{name}}", - "tmuxUnavailable": "tmux ไม่ได้ติดตั้งอยู่บนโฮสต์ระยะไกล จึงใช้เชลล์มาตรฐานแทน", - "tmuxSessionPickerTitle": "เซสชัน tmux", - "tmuxSessionPickerDesc": "พบเซสชัน tmux ที่มีอยู่แล้วในโฮสต์นี้ เลือกหนึ่งเซสชันเพื่อเชื่อมต่อใหม่หรือสร้างเซสชันใหม่", - "tmuxWindows": "วินโดวส์", - "tmuxWindowCount": "{{count}} หน้าต่าง", - "tmuxAttached": "ลูกค้าที่แนบมาด้วย", - "tmuxAttachedCount": "{{count}} แนบมาด้วย", - "tmuxLastActivity": "กิจกรรมล่าสุด", - "tmuxTimeJustNow": "เมื่อสักครู่นี้เอง", - "tmuxTimeMinutes": "{{count}}นาทีที่แล้ว", - "tmuxTimeHours": "{{count}}ชั่วโมงที่แล้ว", - "tmuxTimeDays": "{{count}}วันก่อน", - "tmuxCreateNew": "เริ่มเซสชั่นใหม่", - "tmuxCopyHint": "ปรับตำแหน่งที่เลือกแล้วกด Enter เพื่อคัดลอกไปยังคลิปบอร์ด", "tmuxDetach": "ยกเลิกการเชื่อมต่อจากเซสชัน tmux", - "tmuxDetached": "ตัดการเชื่อมต่อจากเซสชัน tmux แล้ว", - "searchPlaceholder": "หา", - "searchCaseSensitive": "กล่องไม้ขีดไฟ", - "searchWholeWord": "จับคู่คำทั้งหมด", - "searchRegex": "ใช้ Regular Expression", - "searchNoResults": "ไม่พบผลลัพธ์", - "searchResultCount": "{{index}} ของ {{count}}", - "searchNext": "การแข่งขันถัดไป (กดเข้าร่วม)", - "searchPrevious": "การแข่งขันครั้งก่อนหน้า (Shift+Enter)", - "searchClose": "ปิด (หนี)", - "maxReconnectAttemptsReached": "จำนวนครั้งการเชื่อมต่อใหม่สูงสุดครบแล้ว", - "closeTab": "ปิด", - "connectionTimeout": "หมดเวลาการเชื่อมต่อ", "terminalTitle": "เทอร์มินัล - {{host}}", "terminalWithPath": "เทอร์มินัล - {{host}}:{{path}}", "runTitle": "กำลังวิ่ง {{command}} - {{host}}", @@ -1790,27 +1739,14 @@ "opksshOpenBrowser": "เปิดเบราว์เซอร์เพื่อยืนยันตัวตน", "opksshWaitingForAuth": "กำลังรอการยืนยันตัวตนในเบราว์เซอร์...", "opksshAuthenticating": "กำลังตรวจสอบสิทธิ์...", - "opksshTimeout": "การตรวจสอบสิทธิ์หมดเวลา โปรดลองอีกครั้ง", - "opksshAuthFailed": "การตรวจสอบสิทธิ์ล้มเหลว โปรดตรวจสอบข้อมูลประจำตัวของคุณและลองอีกครั้ง", "opksshSignInWith": "ลงชื่อเข้าใช้ด้วย {{provider}}", - "vaultAuthTitle": "ต้องลงชื่อเข้าใช้ Vault", - "vaultAuthDescription": "หน้าต่างสำหรับเข้าสู่ระบบ HashiCorp Vault ได้เปิดขึ้นแล้ว โปรดทำการเข้าสู่ระบบให้เสร็จสมบูรณ์ การเชื่อมต่อนี้จะดำเนินการต่อโดยอัตโนมัติ", - "vaultAuthFailed": "การตรวจสอบสิทธิ์ Vault ล้มเหลว โปรดลองอีกครั้ง", - "vaultReopen": "เปิดหน้าต่างเข้าสู่ระบบอีกครั้ง", "sudoPasswordPopupTitle": "ใส่รหัสผ่าน?", - "linkDialogTitle": "เปิดลิงก์", - "linkDialogOpen": "เปิด", - "linkDialogCopy": "สำเนา", - "websocketAbnormalClose": "การเชื่อมต่อถูกตัดโดยไม่คาดคิด อาจเกิดจากปัญหาเกี่ยวกับรีเวิร์สพร็อกซีหรือการกำหนดค่า SSL โปรดตรวจสอบบันทึกของเซิร์ฟเวอร์", "connectionLogTitle": "บันทึกการเชื่อมต่อ", "connectionLogCopy": "คัดลอกบันทึกไปยังคลิปบอร์ด", "connectionLogEmpty": "ยังไม่มีบันทึกการเชื่อมต่อ", "connectionLogWaiting": "กำลังรอข้อมูลบันทึกการเชื่อมต่อ...", "connectionLogCopied": "บันทึกการเชื่อมต่อถูกคัดลอกไปยังคลิปบอร์ด", "connectionLogCopyFailed": "ไม่สามารถคัดลอกบันทึกไปยังคลิปบอร์ดได้", - "connectionRejected": "เซิร์ฟเวอร์ปฏิเสธการเชื่อมต่อ โปรดตรวจสอบการยืนยันตัวตนและการกำหนดค่าเครือข่ายของคุณ", - "hostKeyRejected": "การตรวจสอบคีย์โฮสต์ SSH ถูกปฏิเสธ การเชื่อมต่อถูกยกเลิก", - "sessionTakenOver": "เซสชั่นนี้เปิดอยู่ในแท็บอื่น กำลังเชื่อมต่อใหม่...", "split": { "splitTab": "แท็บแยก", "addToSplit": "เพิ่มลงในการแบ่ง", @@ -2225,55 +2161,6 @@ "sectionApiKeys": "คีย์ API", "sectionAuditLog": "บันทึกการตรวจสอบ", "sectionSsl": "SSL / Let's Encrypt", - "sectionTouchInput": "การป้อนข้อมูลด้วยระบบสัมผัส", - "touchEnabled": "เปิดใช้งานการป้อนข้อมูลแบบสัมผัส", - "touchEnabledDesc": "อนุญาตให้เลื่อนหน้าจอด้วยนิ้วเดียวในเทอร์มินัล", - "touchMomentumEnabled": "เปิดใช้งานโมเมนตัม", - "touchMomentumEnabledDesc": "เลื่อนหน้าจอต่ออีกสักครู่หลังจากปัดนิ้ว", - "touchAdvanced": "ขั้นสูง", - "touchDragThreshold": "เกณฑ์การลาก", - "touchMaxWheelDelta": "เดลต้าล้อสูงสุด", - "touchMomentumSampleWindow": "หน้าต่างตัวอย่างโมเมนตัม", - "touchReleaseGracePeriod": "ระยะเวลาผ่อนผันการปล่อยตัว", - "touchMinimumVelocity": "ความเร็วต่ำสุด", - "touchMaximumVelocity": "ความเร็วสูงสุด", - "touchMaximumDuration": "ระยะเวลาสูงสุด", - "touchMaximumTravel": "การเดินทางสูงสุด", - "touchDecayTime": "เวลาการเสื่อมสภาพ", - "touchPixelsPerTick": "พิกเซลต่อติ๊ก", - "touchMaximumFrameInterval": "ช่วงเวลาเฟรมสูงสุด", - "touchMaximumTicksPerFrame": "จำนวนขีดสูงสุดต่อเฟรม", - "touchResetDefaults": "รีเซ็ตเป็นการตั้งค่าเริ่มต้น", - "touchSaved": "บันทึกการตั้งค่าการป้อนข้อมูลแบบสัมผัสแล้ว", - "touchSaveFailed": "ไม่สามารถบันทึกการตั้งค่าการป้อนข้อมูลแบบสัมผัสได้", - "sectionImageStorage": "การจัดเก็บรูปภาพ", - "imageStorageMode": "โหมดจัดเก็บข้อมูล", - "imageStorageModeDesc": "ตำแหน่งที่จะบันทึกภาพเทอร์มินัลที่คัดลอกและอัปโหลด การเลือกอัตโนมัติตามการเชื่อมต่อ ทั้งแบบโลคอลและรีโมต (SFTP) จะถูกกำหนดไว้ตายตัว", - "imageStorageModeAuto": "อัตโนมัติ", - "imageStorageModeLocal": "ท้องถิ่น", - "imageStorageModeRemoteSftp": "รีโมท (SFTP)", - "imageStorageLocalDir": "ไดเร็กทอรีท้องถิ่น", - "imageStorageLocalDirDesc": "เส้นทางแบบเต็มบนระบบแบ็กเอนด์ของ Termix จะไม่แสดงหลังจากบันทึกเพื่อความปลอดภัย", - "imageStorageLocalDirPlaceholder": "เว้นว่างไว้เพื่อรักษากระแสไฟฟ้า", - "imageStorageHostPath": "เส้นทางโฮสต์", - "imageStorageHostPathDesc": "มุมมองฝั่งโฮสต์ของไดเร็กทอรีท้องถิ่นที่เอเจนต์เทอร์มินัลได้รับ", - "imageStorageTtl": "ทีทีแอล (มิลลิวินาที)", - "imageStorageMaxCount": "จำนวนภาพสูงสุด", - "imageStorageMaxBytes": "พื้นที่จัดเก็บสูงสุด (ไบต์)", - "imageStorageSaved": "บันทึกการตั้งค่าการจัดเก็บรูปภาพแล้ว", - "imageStorageSaveFailed": "ไม่สามารถบันทึกการตั้งค่าการจัดเก็บรูปภาพได้", - "imageStorageInstanceId": "รหัสอินสแตนซ์เซสชันเทอร์มินัล", - "imageStorageInstanceIdDesc": "รหัสอินสแตนซ์ของแท็บเทอร์มินัลที่เชื่อมต่ออยู่แท็บหนึ่ง ใช้สำหรับทดสอบการมองเห็น", - "imageStorageInstanceIdRequired": "โปรดป้อนรหัสอินสแตนซ์เซสชันเทอร์มินัลก่อน", - "imageStorageTest": "ทดสอบ", - "imageStorageTesting": "กำลังทดสอบ...", - "imageStorageTestFailed": "การทดสอบการจัดเก็บรูปภาพล้มเหลว", - "imageStorageTestConnected": "การเชื่อมต่อเซสชันเทอร์มินัลเสร็จสมบูรณ์แล้ว", - "imageStorageTestNotConnected": "ไม่มีเซสชันเทอร์มินัลที่เชื่อมต่อกับรหัสอินสแตนซ์นี้", - "imageStorageTestSelectedMode": "การอัปโหลดจะใช้รูปแบบ: {{mode}}", - "imageStorageSelectedModeUnavailable": "ไม่พร้อมใช้งาน", - "imageStorageTestLocalVisible": "ไดเร็กทอรีภายในเครื่องสามารถมองเห็นได้จากโฮสต์ที่เชื่อมต่ออยู่", - "imageStorageTestLocalNotVisible": "ไดเร็กทอรีภายในเครื่องจะไม่สามารถมองเห็นได้จากโฮสต์ที่เชื่อมต่ออยู่", "sectionBranding": "ไวท์เลเบล", "brandingAppName": "ชื่อแอปพลิเคชัน", "brandingAppNameDesc": "แสดงบนหน้าจอเข้าสู่ระบบและแท็บเบราว์เซอร์ เว้นว่างไว้เพื่อใช้ \"Termix\"", @@ -2354,9 +2241,6 @@ "oidcSilentLoginDefaultLockedDesc": "การตั้งค่านี้ถูกล็อกโดยตัวแปรสภาพแวดล้อม OIDC_SILENT_LOGIN_DEFAULT และไม่สามารถเปลี่ยนแปลงได้ที่นี่", "allowPasswordReset": "อนุญาตให้รีเซ็ตรหัสผ่าน", "allowPasswordResetDesc": "รีเซ็ตโค้ดผ่านบันทึก Docker", - "commandHistoryEnabled": "ประวัติคำสั่ง", - "commandHistoryEnabledDesc": "อนุญาตให้บันทึกประวัติคำสั่ง เมื่อปิดใช้งาน ประวัติจะไม่ถูกบันทึกไม่ว่าการตั้งค่าต่อโฮสต์จะเป็นอย่างไรก็ตาม", - "updateCommandHistoryFailed": "ไม่สามารถอัปเดตการตั้งค่าประวัติคำสั่งได้", "analyticsEnabled": "แบ่งปันสถิติการใช้งานแบบไม่ระบุตัวตน", "analyticsEnabledDesc": "ส่งรายงานจำนวนผู้ใช้ โฮสต์ และการใช้งานฟีเจอร์รายวันแบบไม่ระบุตัวตน เพื่อช่วยปรับปรุง Termix ไม่มีข้อมูลส่วนบุคคลหรือรายละเอียดการเชื่อมต่อใดๆ รวมอยู่ด้วย", "analyticsEnabledLockedDesc": "การตั้งค่านี้ถูกล็อกโดยตัวแปรสภาพแวดล้อม ENABLE_TELEMETRY และไม่สามารถเปลี่ยนแปลงได้ที่นี่", @@ -2647,11 +2531,6 @@ "hostDefaultsTerminal": "เทอร์มินัล", "hostDefaultsAutoTmux": "ออโต้-ทมักซ์", "hostDefaultsAutoTmuxDesc": "เรียกใช้เทอร์มินัลของโฮสต์ใหม่ภายใน tmux เพื่อให้เซสชันยังคงใช้งานได้แม้การเชื่อมต่อถูกตัด (ต้องมี tmux บนโฮสต์)", - "terminalSessionTimeout": "การคงอยู่ของเซสชันเทอร์มินัล", - "terminalSessionTimeoutDesc": "ระยะเวลาที่เทอร์มินัลที่ไม่ได้เชื่อมต่อจะยังคงทำงานอยู่บนเซิร์ฟเวอร์ก่อนที่จะถูกปิด ต่ำสุด 1 นาที สูงสุด 1440 นาที สำหรับเซสชันที่ต้องใช้งานนานกว่านี้ ให้เปิดใช้งาน Auto-Tmux บนโฮสต์", - "terminalSessionTimeoutRange": "ป้อนค่าระหว่าง 1 ถึง 1440 นาที", - "terminalSessionTimeoutSaved": "บันทึกการคงอยู่ของเซสชันเทอร์มินัลแล้ว", - "terminalSessionTimeoutSaveFailed": "ไม่สามารถบันทึกการคงอยู่ของเซสชันเทอร์มินัลได้", "minutes": "นาที", "hostDefaultsSessionLogging": "การบันทึกเซสชัน", "hostDefaultsSessionLoggingDesc": "บันทึกเซสชันเทอร์มินัลบนโฮสต์ใหม่โดยค่าเริ่มต้น", @@ -2708,15 +2587,6 @@ "connectToTerminal": "เชื่อมต่อกับเทอร์มินัล", "connectToFiles": "เชื่อมต่อกับไฟล์" }, - "history": { - "noTerminalSelected": "ไม่ได้เลือกเทอร์มินัล", - "noTerminalSelectedHint": "เปิดแท็บเทอร์มินัล SSH เพื่อดูประวัติคำสั่ง", - "searchPlaceholder": "ประวัติการค้นหา...", - "clearAll": "ล้างทั้งหมด", - "noHistoryEntries": "ไม่มีรายการประวัติ", - "trackingDisabled": "ปิดใช้งานการติดตามประวัติแล้ว", - "trackingDisabledHint": "เปิดใช้งานในตั้งค่าเทอร์มินัลของเครื่องโฮสต์" - }, "sshTools": { "keyRecordingTitle": "การบันทึกคีย์", "recordToTerminals": "บันทึกไปยังเทอร์มินัล", @@ -3178,30 +3048,11 @@ "terminalToolbar": { "title": "แถบเครื่องมือเทอร์มินัล", "showActions": "แสดงแถบเครื่องมือ", - "hideToolbar": "ซ่อนแถบเครื่องมือ", - "showToolbar": "ขยายแถบเครื่องมือ", - "moveToolbar": "ย้ายแถบเครื่องมือ", - "moveToolbarHint": "ลากเพื่อจัดตำแหน่งแถบเครื่องมือใหม่", - "layout": "โหมดแสดงแถบเครื่องมือ", - "layoutIcon": "เฉพาะไอคอน", - "layoutLabeled": "ไอคอนและป้ายกำกับ", - "layoutExpanded": "ขยายเพิ่มเติมด้วยตัวชี้วัด", - "image": "การดำเนินการกับรูปภาพ (เฉพาะรูปภาพ)", - "upload": "อัปโหลดรูปภาพ", - "paste": "วางรูปภาพ", - "detachTmux": "ถอด tmux ออก", - "detachTmuxDescription": "ยกเลิกการเชื่อมต่อเซสชัน tmux", - "imageActionFailed": "การดำเนินการรูปภาพล้มเหลว", - "retry": "ลองใหม่อีกครั้ง", - "dismissError": "แก้ไขข้อผิดพลาดเกี่ยวกับรูปภาพ", "cpu": "ซีพียู", "memory": "หน่วยความจำ", "disk": "ดิสก์", "statsUnavailable": "ไม่สามารถดูสถิติแบบเรียลไทม์ได้", "tmuxDetach": "ยกเลิกการเชื่อมต่อเซสชัน tmux", - "uploadImage": "อัปโหลดรูปภาพไปยังโฮสต์", - "uploadingImage": "กำลังอัปโหลด...", - "pasteImage": "วางภาพจากคลิปบอร์ด (เฉพาะภาพ)", "openFiles": "เปิดไฟล์", "openDocker": "เปิด Docker", "openTunnel": "อุโมงค์เปิด", @@ -3285,31 +3136,6 @@ "timeHours": "{{count}}ชั่วโมงที่แล้ว", "timeDays": "{{count}}วันก่อน" }, - "mobileKeyboard": { - "shift": "กะ", - "ctrl": "Ctrl", - "esc": "เอส", - "tab": "แท็บ", - "backTab": "⇥", - "arrowUp": "ลูกศรขึ้น", - "arrowDown": "ลูกศรลง", - "arrowLeft": "ลูกศรซ้าย", - "arrowRight": "ลูกศรขวา", - "home": "บ้าน", - "end": "จบ", - "pageUp": "พีจีอัพ", - "pageDown": "พีจีดีเอ็น", - "delete": "เดล", - "paste": "แปะ", - "editQuickKeys": "แก้ไขปุ่มลัด", - "quickKeysTitle": "ปุ่มลัด", - "quickKeysDesc": "แตะ × เพื่อลบ รองรับได้สูงสุด 8 ตัวอักษร", - "quickKeyPlaceholder": "เช่น sudo ", - "addQuickKey": "เพิ่ม", - "removeQuickKey": "ลบ", - "resetDefaults": "รีเซ็ตเป็นการตั้งค่าเริ่มต้น", - "done": "เสร็จแล้ว" - }, "serial": { "title": "ซีเรียล", "portLabel": "ท่าเรือ", diff --git a/src/ui/locales/translated/tr_TR.json b/src/ui/locales/translated/tr_TR.json index c0d997849..fd8ab2d81 100644 --- a/src/ui/locales/translated/tr_TR.json +++ b/src/ui/locales/translated/tr_TR.json @@ -742,7 +742,6 @@ "addHost": "Ana Bilgisayar Ekle", "editHost": "Ana Bilgisayarı Düzenle", "cloneHost": "Ana Bilgisayarı Kopyala", - "enableTerminal": "Terminali Etkinleştir", "enableTunnel": "Tüneli Etkinleştir", "enableFileManager": "Dosya Yöneticisini Etkinleştir", "enableDocker": "Docker'ı Etkinleştir", @@ -1041,8 +1040,6 @@ "enableSessionLoggingDesc": "Daha sonra gözden geçirmek için terminal oturumu çıktısını kaydet", "allowSessionSharing": "Oturum Paylaşımına İzin Ver", "allowSessionSharingDesc": "Bu sunucudaki canlı oturumların bağlantı yoluyla veya diğer kullanıcılarla paylaşılmasına izin verin.", - "enableCommandHistory": "Komut Geçmişi", - "enableCommandHistoryDesc": "Geçmiş ve otomatik tamamlama için bu terminalde çalıştırılan komutları kaydet", "localEchoLabel": "Yerel Yankı", "localEchoDesc": "Yavaş bağlantılarda bile yerel olarak güvenli yazdırılabilir tuş vuruşlarını tahmin edin. Parola istemleri ve kontrol girişleri asla tekrarlanmaz.", "localEchoDefault": "Genel ayarı kullanın", @@ -1072,8 +1069,6 @@ "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", - "enableTerminalToolbar": "Terminal Araç Çubuğunu Etkinleştir", - "enableTerminalToolbarDesc": "Etkin terminale sabitlenmiş, bu sunucunun araçlarına ve canlı istatistiklerine hızlı erişim sağlayan bir araç çubuğu gösterin.", "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.", @@ -1550,9 +1545,7 @@ "links": "Bağlantılar", "navigate": "Gezin", "select": "Seç", - "toggleWith": "Geçiş Yap", - "localTerminal": "Yerel Terminal", - "localTerminalDesc": "Bu bilgisayarda bir kabuk açın." + "toggleWith": "Geçiş Yap" }, "splitScreen": { "paneEmpty": "Bölme {{index}} - boş", @@ -1717,57 +1710,13 @@ "loadSharesFailed": "Aktif paylaşımlar yüklenemedi." }, "terminal": { - "sessionExpiredNotice": "{{host}} üzerindeki önceki oturumunuz {{minutes}} dakika sonra sona erdi; kabuk ve içinde çalışan her şey silindi. Oturumların aktif kalması için bu sunucuda Auto-Tmux'u etkinleştirin.", - "sessionExpiredNoticeNoMinutes": "{{host}} üzerindeki önceki oturumunuz bağlantı kesikken sona erdi; kabuk ve içinde çalışan her şey kayboldu. Oturumların aktif kalması için bu sunucuda Auto-Tmux'u etkinleştirin.", - "enableAutoTmuxAction": "Otomatik Tmux'ı etkinleştirin", - "autoTmuxEnabled": "{{host}}için otomatik Tmux etkinleştirildi. Bu, sonraki bağlantıdan itibaren geçerli olacak.", - "autoTmuxEnableFailed": "Otomatik Tmux etkinleştirme başarısız oldu.", "connect": "Sunucuya Bağlan", "clear": "Temizle", "paste": "Yapıştır", - "reconnect": "Yeniden Bağlan", - "connectionLost": "Bağlantı kesildi", "connected": "Bağlandı", "clipboardWriteFailed": "Panoya kopyalama başarısız oldu. Sayfanın HTTPS veya localhost üzerinden sunulduğundan emin olun.", "clipboardReadFailed": "Panodan okuma başarısız oldu. Pano izinlerinin verildiğinden emin olun.", - "clipboardHttpWarning": "Panoyu okuyamadım. Bazı tarayıcılar HTTPS dışında panoya erişimi kısıtlar; bunun yerine Ctrl+V tuş kombinasyonunu deneyin veya Termix'i HTTPS üzerinden sunun.", - "passwordPromptFillTitle": "Kayıtlı parola bu isteme doldurulsun mu?", - "unknownError": "Bilinmeyen bir hata oluştu", - "websocketError": "WebSocket bağlantı hatası", - "connecting": "Bağlanıyor...", - "noHostSelected": "Hiçbir sunucu seçilmedi", - "reconnecting": "Yeniden bağlanıyor... ({{attempt}}/{{max}})", - "reconnected": "Başarıyla yeniden bağlandı", - "tmuxSessionCreated": "tmux oturumu oluşturuldu: {{name}}", - "tmuxSessionAttached": "tmux oturumuna bağlanıldı: {{name}}", - "tmuxUnavailable": "Uzak sunucuda tmux yüklü değil, standart kabuğa dönülüyor", - "tmuxSessionPickerTitle": "tmux Oturumları", - "tmuxSessionPickerDesc": "Bu sunucuda mevcut tmux oturumları bulundu. Yeniden bağlanmak için birini seçin veya yeni bir oturum oluşturun.", - "tmuxWindows": "Pencereler", - "tmuxWindowCount": "{{count}} pencere", - "tmuxAttached": "Bağlı istemciler", - "tmuxAttachedCount": "{{count}} bağlı", - "tmuxLastActivity": "Son etkinlik", - "tmuxTimeJustNow": "az önce", - "tmuxTimeMinutes": "{{count}}dk önce", - "tmuxTimeHours": "{{count}}s önce", - "tmuxTimeDays": "{{count}}g önce", - "tmuxCreateNew": "Yeni oturum başlat", - "tmuxCopyHint": "Seçimi ayarlayın ve panoya kopyalamak için Enter tuşuna basın", "tmuxDetach": "tmux oturumundan ayrıl", - "tmuxDetached": "tmux oturumundan ayrıldı", - "searchPlaceholder": "Bulmak", - "searchCaseSensitive": "Kibrit Kutusu", - "searchWholeWord": "Kelimenin tamamını eşleştir", - "searchRegex": "Düzenli İfade Kullanımı", - "searchNoResults": "Sonuç bulunamadı.", - "searchResultCount": "{{index}} / {{count}}", - "searchNext": "Sonraki Maç (Giriş)", - "searchPrevious": "Önceki Maç (Shift+Enter)", - "searchClose": "Kapat (Escape)", - "maxReconnectAttemptsReached": "Maksimum yeniden bağlanma denemesine ulaşıldı", - "closeTab": "Kapat", - "connectionTimeout": "Bağlantı zaman aşımı", "terminalTitle": "Terminal - {{host}}", "terminalWithPath": "Terminal - {{host}}:{{path}}", "runTitle": "Çalıştırılıyor: {{command}} - {{host}}", @@ -1790,27 +1739,14 @@ "opksshOpenBrowser": "Kimlik Doğrulaması için Tarayıcı Aç", "opksshWaitingForAuth": "Tarayıcıda kimlik doğrulaması bekleniyor...", "opksshAuthenticating": "Kimlik doğrulaması işleniyor...", - "opksshTimeout": "Kimlik doğrulaması zaman aşımına uğradı. Lütfen tekrar deneyin.", - "opksshAuthFailed": "Kimlik doğrulaması başarısız oldu. Lütfen kimlik bilgilerinizi kontrol edip tekrar deneyin.", "opksshSignInWith": "{{provider}} ile oturum aç", - "vaultAuthTitle": "Vault Oturum Açması Gerekli", - "vaultAuthDescription": "HashiCorp Vault'ta oturum açmak için bir pencere açıldı. Orada oturum açmayı tamamlayın; bu bağlantı otomatik olarak devam edecek.", - "vaultAuthFailed": "Vault kimlik doğrulaması başarısız oldu. Lütfen tekrar deneyin.", - "vaultReopen": "Oturum açma penceresini yeniden aç", "sudoPasswordPopupTitle": "Parola Girin?", - "linkDialogTitle": "Bağlantıyı Aç", - "linkDialogOpen": "Aç", - "linkDialogCopy": "Kopyala", - "websocketAbnormalClose": "Bağlantı beklenmedik şekilde kapandı. Bu, ters proxy veya SSL yapılandırma sorunundan kaynaklanabilir. Lütfen sunucu günlüklerini kontrol edin.", "connectionLogTitle": "Bağlantı Günlüğü", "connectionLogCopy": "Günlükleri panoya kopyala", "connectionLogEmpty": "Henüz bağlantı günlüğü yok", "connectionLogWaiting": "Bağlantı günlükleri bekleniyor...", "connectionLogCopied": "Bağlantı günlükleri panoya kopyalandı", "connectionLogCopyFailed": "Günlükler panoya kopyalanamadı", - "connectionRejected": "Bağlantı sunucu tarafından reddedildi. Lütfen kimlik doğrulamanızı ve ağ yapılandırmanızı kontrol edin.", - "hostKeyRejected": "SSH sunucu anahtarı doğrulaması reddedildi. Bağlantı iptal edildi.", - "sessionTakenOver": "Oturum başka bir sekmede açıldı. Yeniden bağlanılıyor...", "split": { "splitTab": "Sekmeyi Böl", "addToSplit": "Bölünmüşe Ekle", @@ -2225,55 +2161,6 @@ "sectionApiKeys": "API Anahtarları", "sectionAuditLog": "Denetim Günlüğü", "sectionSsl": "SSL / Let's Encrypt", - "sectionTouchInput": "Dokunmatik Giriş", - "touchEnabled": "Dokunmatik Girişi Etkinleştir", - "touchEnabledDesc": "Terminallerde tek parmakla dokunmatik kaydırmaya izin verin.", - "touchMomentumEnabled": "İvme Kazandırın", - "touchMomentumEnabledDesc": "Kaydırma işleminden sonra kısa bir süre daha aşağı kaydırmaya devam edin.", - "touchAdvanced": "Gelişmiş", - "touchDragThreshold": "Sürükleme eşiği", - "touchMaxWheelDelta": "Maksimum tekerlek deltası", - "touchMomentumSampleWindow": "Momentum örnek penceresi", - "touchReleaseGracePeriod": "Serbest bırakma erteleme süresi", - "touchMinimumVelocity": "Minimum hız", - "touchMaximumVelocity": "Maksimum hız", - "touchMaximumDuration": "Maksimum süre", - "touchMaximumTravel": "Maksimum seyahat", - "touchDecayTime": "Bozunma süresi", - "touchPixelsPerTick": "Piksel başına tik", - "touchMaximumFrameInterval": "Maksimum kare aralığı", - "touchMaximumTicksPerFrame": "Kare başına maksimum tik sayısı", - "touchResetDefaults": "Varsayılan ayarlara sıfırla", - "touchSaved": "Dokunmatik giriş ayarları kaydedildi.", - "touchSaveFailed": "Dokunmatik giriş ayarları kaydedilemedi.", - "sectionImageStorage": "Görüntü Depolama", - "imageStorageMode": "Depolama Modu", - "imageStorageModeDesc": "Yapıştırılan ve yüklenen terminal görüntülerinin yazıldığı yer. Bağlantı türüne göre otomatik seçim, Yerel ve Uzak (SFTP) ayarları düzeltildi.", - "imageStorageModeAuto": "Otomatik", - "imageStorageModeLocal": "Yerel", - "imageStorageModeRemoteSftp": "Uzaktan (SFTP)", - "imageStorageLocalDir": "Yerel dizin", - "imageStorageLocalDirDesc": "Termix arka ucundaki mutlak yol. Güvenlik nedeniyle kaydedildikten sonra gösterilmez.", - "imageStorageLocalDirPlaceholder": "Güncel kalmak için boş bırakın.", - "imageStorageHostPath": "Ana bilgisayar yolu", - "imageStorageHostPathDesc": "Terminal aracıların aldığı yerel dizinin sunucu tarafındaki görünümü.", - "imageStorageTtl": "TTL (ms)", - "imageStorageMaxCount": "Maksimum görüntü sayısı", - "imageStorageMaxBytes": "Maksimum depolama baytları", - "imageStorageSaved": "Görüntü depolama ayarları kaydedildi.", - "imageStorageSaveFailed": "Görüntü depolama ayarları kaydedilemedi.", - "imageStorageInstanceId": "Terminal oturum örneği kimliği", - "imageStorageInstanceIdDesc": "Görünürlüğü test etmek için kullanılan, bağlı terminal sekmelerinizden birinin örnek kimliği.", - "imageStorageInstanceIdRequired": "Öncelikle bir terminal oturum örneği kimliği girin.", - "imageStorageTest": "Test", - "imageStorageTesting": "Test...", - "imageStorageTestFailed": "Görüntü depolama testi başarısız oldu.", - "imageStorageTestConnected": "Terminal oturumu bağlandı.", - "imageStorageTestNotConnected": "Bu örnek kimliğiyle bağlantılı bir terminal oturumu bulunmamaktadır.", - "imageStorageTestSelectedMode": "Yükleme işlemi şu şekilde yapılır: {{mode}}", - "imageStorageSelectedModeUnavailable": "Mevcut değil", - "imageStorageTestLocalVisible": "Yerel dizin, bağlı sunucudan görülebilir.", - "imageStorageTestLocalNotVisible": "Yerel dizin, bağlı sunucudan görünmez.", "sectionBranding": "Beyaz Etiket", "brandingAppName": "Uygulama Adı", "brandingAppNameDesc": "Giriş ekranında ve tarayıcı sekmesinde gösterilir. \"Termix\" kullanmak için boş bırakın.", @@ -2354,9 +2241,6 @@ "oidcSilentLoginDefaultLockedDesc": "Bu ayar, OIDC_SILENT_LOGIN_DEFAULT ortam değişkeni tarafından kilitlenmiştir ve burada değiştirilemez.", "allowPasswordReset": "Parola Sıfırlamaya İzin Ver", "allowPasswordResetDesc": "Docker günlükleri aracılığıyla sıfırlama kodu", - "commandHistoryEnabled": "Komut Geçmişi", - "commandHistoryEnabledDesc": "Komut geçmişi kaydına izin ver. Devre dışı bırakıldığında, host başına ayarlara bakılmaksızın geçmiş kaydedilmez.", - "updateCommandHistoryFailed": "Komut geçmişi ayarı güncellenemedi", "analyticsEnabled": "Anonim Kullanım İstatistiklerini Paylaşın", "analyticsEnabledDesc": "Termix'i geliştirmeye yardımcı olmak için kullanıcı, sunucu ve özellik kullanımına ilişkin anonim günlük sayım gönderir. Hiçbir kişisel veri veya bağlantı bilgisi asla dahil edilmez.", "analyticsEnabledLockedDesc": "Bu ayar, ENABLE_TELEMETRY ortam değişkeni tarafından kilitlenmiştir ve burada değiştirilemez.", @@ -2647,11 +2531,6 @@ "hostDefaultsTerminal": "Terminal", "hostDefaultsAutoTmux": "Otomatik Tmux", "hostDefaultsAutoTmuxDesc": "Yeni sunucuların terminallerini tmux içinde çalıştırın, böylece oturumlar bağlantı kesintilerinden etkilenmeden devam eder (sunucuda tmux kurulu olması gerekir).", - "terminalSessionTimeout": "Terminal Oturum Kalıcılığı", - "terminalSessionTimeoutDesc": "Bağlantısı kesilmiş bir terminalin sunucuda kapatılmadan önce ne kadar süreyle aktif tutulacağı. Minimum 1 dakika, maksimum 1440 dakika. Bu süreyi aşması gereken oturumlar için, sunucuda Auto-Tmux'u etkinleştirin.", - "terminalSessionTimeoutRange": "1 ile 1440 dakika arasında bir değer girin.", - "terminalSessionTimeoutSaved": "Terminal oturumu kalıcı olarak kaydedildi.", - "terminalSessionTimeoutSaveFailed": "Terminal oturumunun kalıcılığını kaydetme başarısız oldu.", "minutes": "dakika", "hostDefaultsSessionLogging": "Oturum Günlüğü", "hostDefaultsSessionLoggingDesc": "Yeni ana bilgisayarlarda varsayılan olarak terminal oturumlarını kaydet", @@ -2708,15 +2587,6 @@ "connectToTerminal": "Terminale Bağlan", "connectToFiles": "Dosyalara Bağlan" }, - "history": { - "noTerminalSelected": "Terminal seçilmedi", - "noTerminalSelectedHint": "Komut geçmişini görüntülemek için bir SSH terminal sekmesi açın", - "searchPlaceholder": "Geçmişte ara...", - "clearAll": "Tümünü Temizle", - "noHistoryEntries": "Geçmiş girdisi yok", - "trackingDisabled": "Geçmiş takibi kapalı", - "trackingDisabledHint": "Bunu sunucunun terminal ayarlarından etkinleştirin." - }, "sshTools": { "keyRecordingTitle": "Tuş Kaydı", "recordToTerminals": "Terminallere Kaydet", @@ -3178,30 +3048,11 @@ "terminalToolbar": { "title": "Terminal Araç Çubuğu", "showActions": "Araç çubuğunu göster", - "hideToolbar": "Araç çubuğunu gizle", - "showToolbar": "Araç çubuğunu genişletin", - "moveToolbar": "Araç çubuğunu taşı", - "moveToolbarHint": "Araç çubuğunu yeniden konumlandırmak için sürükleyin.", - "layout": "Araç çubuğu görüntüleme modu", - "layoutIcon": "Yalnızca simge", - "layoutLabeled": "Simgeler ve etiketler", - "layoutExpanded": "Ölçütlerle genişletildi", - "image": "Görüntü işlemleri (yalnızca görüntüler)", - "upload": "Resim yükle", - "paste": "Resmi yapıştır", - "detachTmux": "tmux'u ayır", - "detachTmuxDescription": "Tmux oturumunu ayır", - "imageActionFailed": "Görüntü işlemi başarısız oldu", - "retry": "Tekrar dene", - "dismissError": "Görüntü hatasını kapat", "cpu": "İşlemci", "memory": "Hafıza", "disk": "Disk", "statsUnavailable": "Canlı istatistikler mevcut değil.", "tmuxDetach": "Tmux oturumunu ayır", - "uploadImage": "Sunucuya bir resim yükleyin.", - "uploadingImage": "Yükleniyor...", - "pasteImage": "Panodan resim yapıştır (yalnızca resimler)", "openFiles": "Dosyaları Aç", "openDocker": "Open Docker", "openTunnel": "Açık Tünel", @@ -3285,31 +3136,6 @@ "timeHours": "{{count}} saat önce", "timeDays": "{{count}} gün önce" }, - "mobileKeyboard": { - "shift": "Shift", - "ctrl": "Ctrl", - "esc": "Esc", - "tab": "Tab", - "backTab": "⇥", - "arrowUp": "Yukarı Ok", - "arrowDown": "Aşağı Ok", - "arrowLeft": "Sol Ok", - "arrowRight": "Sağ Ok", - "home": "Home", - "end": "End", - "pageUp": "PgUp", - "pageDown": "PgDn", - "delete": "Del", - "paste": "Yapıştır", - "editQuickKeys": "Hızlı tuşları düzenle", - "quickKeysTitle": "Hızlı Tuşlar", - "quickKeysDesc": "Kaldırmak için × işaretine dokunun. En fazla 8 karakter desteklenir.", - "quickKeyPlaceholder": "ör. sudo ", - "addQuickKey": "Ekle", - "removeQuickKey": "Kaldır", - "resetDefaults": "Varsayılanlara sıfırla", - "done": "Tamam" - }, "serial": { "title": "Seri", "portLabel": "Port", diff --git a/src/ui/locales/translated/uk_UA.json b/src/ui/locales/translated/uk_UA.json index 00c7d8f85..314025822 100644 --- a/src/ui/locales/translated/uk_UA.json +++ b/src/ui/locales/translated/uk_UA.json @@ -742,7 +742,6 @@ "addHost": "Додати хоста", "editHost": "Редагувати хост", "cloneHost": "Клон хоста", - "enableTerminal": "Увімкнути термінал", "enableTunnel": "Увімкнути тунель", "enableFileManager": "Увімкнути файловий менеджер", "enableDocker": "Увімкнути Докер", @@ -1041,8 +1040,6 @@ "enableSessionLoggingDesc": "Запис виводу сеансу терміналу для подальшого перегляду", "allowSessionSharing": "Дозволити спільний доступ до сеансу", "allowSessionSharingDesc": "Дозволити ділитися прямими трансляціями на цьому хості через посилання або з іншими користувачами", - "enableCommandHistory": "Історія команд", - "enableCommandHistoryDesc": "Запис команд, що виконуються в цьому терміналі, для історії та автозаповнення", "localEchoLabel": "Локальне відлуння", "localEchoDesc": "Передбачати безпечні для друку натискання клавіш локально на повільних з'єднаннях. Запити на введення пароля та введені елементи керування ніколи не відображаються.", "localEchoDefault": "Використовувати глобальні налаштування", @@ -1072,8 +1069,6 @@ "bindHost": "Прив’язати хост", "enableTmuxMonitor": "Увімкнути монітор Tmux", "enableTmuxMonitorDesc": "Показати цей хост у моніторі Tmux та додати його дії tmux до бічної панелі", - "enableTerminalToolbar": "Увімкнути панель інструментів термінала", - "enableTerminalToolbarDesc": "Показати панель швидкого доступу, прикріплену до активного терміналу, для інструментів цього хоста та актуальної статистики", "connectionToolbar": "Панель інструментів з’єднання", "showConnectionToolbar": "Показати панель інструментів з’єднань", "showConnectionToolbarDesc": "Показувати панель інструментів у сеансах SSH, RDP, VNC та Telnet цього хоста. Ви також можете тимчасово приховати її з самої панелі інструментів.", @@ -1550,9 +1545,7 @@ "links": "Посилання", "navigate": "Навігація", "select": "Виберіть", - "toggleWith": "Перемикання за допомогою", - "localTerminal": "Місцевий термінал", - "localTerminalDesc": "Відкрийте оболонку на цьому комп'ютері" + "toggleWith": "Перемикання за допомогою" }, "splitScreen": { "paneEmpty": "Панель {{index}} – порожня", @@ -1717,57 +1710,13 @@ "loadSharesFailed": "Не вдалося завантажити активні спільні ресурси" }, "terminal": { - "sessionExpiredNotice": "Ваш попередній сеанс на {{host}} завершився через {{minutes}} хвилин після відключення; оболонка та все, що в ній працює, зникли. Увімкніть Auto-Tmux на цьому хості, щоб підтримувати сеанси активними.", - "sessionExpiredNoticeNoMinutes": "Ваш попередній сеанс на {{host}} закінчився під час відключення; оболонка та все, що в ній працює, зникли. Увімкніть Auto-Tmux на цьому хості, щоб підтримувати сеанси активними.", - "enableAutoTmuxAction": "Увімкнути автоматичне Tmux", - "autoTmuxEnabled": "Автоматичне Tmux-змінення увімкнено для {{host}}. Застосовується з наступного підключення.", - "autoTmuxEnableFailed": "Не вдалося ввімкнути Auto-Tmux", "connect": "Підключення до хоста", "clear": "Очистити", "paste": "Вставити", - "reconnect": "Знову підключитися", - "connectionLost": "З’єднання втрачено", "connected": "Підключено", "clipboardWriteFailed": "Не вдалося скопіювати в буфер обміну. Переконайтеся, що сторінка обслуговується через HTTPS або localhost.", "clipboardReadFailed": "Не вдалося прочитати з буфера обміну. Переконайтеся, що надано дозволи на доступ до буфера обміну.", - "clipboardHttpWarning": "Не вдалося прочитати буфер обміну. Деякі браузери обмежують доступ до буфера обміну поза HTTPS — спробуйте Ctrl+V або обслуговуйте Termix через HTTPS.", - "passwordPromptFillTitle": "Введіть збережений пароль у цей запит?", - "unknownError": "Сталася невідома помилка", - "websocketError": "Помилка підключення WebSocket", - "connecting": "Підключення...", - "noHostSelected": "Хост не вибрано", - "reconnecting": "Повторне підключення... ({{attempt}}/{{max}})", - "reconnected": "Успішно відновлено підключення", - "tmuxSessionCreated": "Створено сеанс tmux: {{name}}", - "tmuxSessionAttached": "додано сеанс tmux: {{name}}", - "tmuxUnavailable": "tmux не встановлено на віддаленому хості, повертається до стандартної оболонки", - "tmuxSessionPickerTitle": "Сесії tmux", - "tmuxSessionPickerDesc": "На цьому хості знайдено існуючі сеанси tmux. Виберіть один для повторного підключення або створення нового сеансу.", - "tmuxWindows": "Вікна", - "tmuxWindowCount": "вікно {{count}}", - "tmuxAttached": "Підключені клієнти", - "tmuxAttachedCount": "{{count}} додається", - "tmuxLastActivity": "Остання активність", - "tmuxTimeJustNow": "щойно", - "tmuxTimeMinutes": "{{count}}хв тому", - "tmuxTimeHours": "{{count}}год тому", - "tmuxTimeDays": "{{count}}днів тому", - "tmuxCreateNew": "Почати новий сеанс", - "tmuxCopyHint": "Відрегулюйте виділення та натисніть Enter, щоб скопіювати його в буфер обміну", "tmuxDetach": "Від’єднатися від сеансу tmux", - "tmuxDetached": "Від’єднано від сеансу tmux", - "searchPlaceholder": "Знайти", - "searchCaseSensitive": "Зіставте регістр", - "searchWholeWord": "Зіставлення цілого слова", - "searchRegex": "Використовуйте регулярний вираз", - "searchNoResults": "Немає результатів", - "searchResultCount": "{{index}} з {{count}}", - "searchNext": "Наступний матч (Enter)", - "searchPrevious": "Попередній збіг (Shift+Enter)", - "searchClose": "Закрити (Escape)", - "maxReconnectAttemptsReached": "Досягнуто максимальної кількості спроб повторного підключення", - "closeTab": "Закрити", - "connectionTimeout": "Тайм-аут з'єднання", "terminalTitle": "Термінал - {{host}}", "terminalWithPath": "Термінал - {{host}}:{{path}}", "runTitle": "Запуск {{command}} - {{host}}", @@ -1790,27 +1739,14 @@ "opksshOpenBrowser": "Відкрийте браузер для автентифікації", "opksshWaitingForAuth": "Очікування автентифікації у браузері...", "opksshAuthenticating": "Обробка автентифікації...", - "opksshTimeout": "Час очікування автентифікації минув. Спробуйте ще раз.", - "opksshAuthFailed": "Помилка автентифікації. Перевірте свої облікові дані та спробуйте ще раз.", "opksshSignInWith": "Увійти за допомогою {{provider}}", - "vaultAuthTitle": "Потрібен вхід до Сейфа", - "vaultAuthDescription": "Відкрилося вікно для входу в HashiCorp Vault. Завершіть вхід там; це з’єднання буде продовжено автоматично.", - "vaultAuthFailed": "Не вдалося виконати автентифікацію сховища. Спробуйте ще раз.", - "vaultReopen": "Знову відкрити вікно входу", "sudoPasswordPopupTitle": "Ввести пароль?", - "linkDialogTitle": "Відкрити посилання", - "linkDialogOpen": "ВІДЧИНЕНО", - "linkDialogCopy": "Копіювати", - "websocketAbnormalClose": "З’єднання несподівано завершилося. Можливо, це пов’язано зі зворотним проксі-сервером або проблемою конфігурації SSL. Перевірте журнали сервера.", "connectionLogTitle": "Журнал підключення", "connectionLogCopy": "Копіювати журнали в буфер обміну", "connectionLogEmpty": "Журналів підключень поки що немає", "connectionLogWaiting": "Очікування журналів підключення...", "connectionLogCopied": "Журнали підключень скопійовано в буфер обміну", "connectionLogCopyFailed": "Не вдалося скопіювати журнали в буфер обміну", - "connectionRejected": "З’єднання відхилено сервером. Перевірте автентифікацію та конфігурацію мережі.", - "hostKeyRejected": "Перевірку ключа хоста SSH відхилено. З'єднання скасовано.", - "sessionTakenOver": "Сеанс відкрито в іншій вкладці. Повторне підключення...", "split": { "splitTab": "Розділити вкладку", "addToSplit": "Додати до Спліта", @@ -2225,55 +2161,6 @@ "sectionApiKeys": "Ключі API", "sectionAuditLog": "Журнал аудиту", "sectionSsl": "SSL / Давайте шифрувати", - "sectionTouchInput": "Сенсорний ввід", - "touchEnabled": "Увімкнути сенсорне введення", - "touchEnabledDesc": "Дозволити прокручування одним пальцем у терміналах", - "touchMomentumEnabled": "Увімкнути Momentum", - "touchMomentumEnabledDesc": "Продовжуйте коротко гортати після проведення пальцем", - "touchAdvanced": "Розширений", - "touchDragThreshold": "Поріг перетягування", - "touchMaxWheelDelta": "Максимальна дельта коліс", - "touchMomentumSampleWindow": "Вікно вибірки імпульсу", - "touchReleaseGracePeriod": "Пільговий період випуску", - "touchMinimumVelocity": "Мінімальна швидкість", - "touchMaximumVelocity": "Максимальна швидкість", - "touchMaximumDuration": "Максимальна тривалість", - "touchMaximumTravel": "Максимальний хід", - "touchDecayTime": "Час розпаду", - "touchPixelsPerTick": "Пікселів на такт", - "touchMaximumFrameInterval": "Максимальний інтервал кадру", - "touchMaximumTicksPerFrame": "Максимальна кількість тиків на кадр", - "touchResetDefaults": "Скинути до налаштувань за замовчуванням", - "touchSaved": "Налаштування сенсорного введення збережено", - "touchSaveFailed": "Не вдалося зберегти налаштування сенсорного введення", - "sectionImageStorage": "Зберігання зображень", - "imageStorageMode": "Режим зберігання", - "imageStorageModeDesc": "Куди записуються вставлені та завантажені образи терміналів. Виправлено автоматичний вибір за підключенням, локальним та віддаленим (SFTP).", - "imageStorageModeAuto": "Авто", - "imageStorageModeLocal": "Місцевий", - "imageStorageModeRemoteSftp": "Віддалений (SFTP)", - "imageStorageLocalDir": "Локальний каталог", - "imageStorageLocalDirDesc": "Абсолютний шлях на серверній частині Termix. Не відображається після збереження з міркувань безпеки.", - "imageStorageLocalDirPlaceholder": "Залиште порожнім, щоб зберегти актуальність", - "imageStorageHostPath": "Шлях хоста", - "imageStorageHostPathDesc": "Вигляд локального каталогу, який отримують агенти терміналів, з боку хоста.", - "imageStorageTtl": "TTL (мс)", - "imageStorageMaxCount": "Максимальна кількість зображень", - "imageStorageMaxBytes": "Максимальна кількість байтів пам'яті", - "imageStorageSaved": "Налаштування зберігання зображень збережено", - "imageStorageSaveFailed": "Не вдалося зберегти налаштування сховища зображень", - "imageStorageInstanceId": "Ідентифікатор екземпляра термінального сеансу", - "imageStorageInstanceIdDesc": "Ідентифікатор екземпляра однієї з ваших підключених вкладок терміналу, що використовується для перевірки видимості.", - "imageStorageInstanceIdRequired": "Спочатку введіть ідентифікатор екземпляра сеансу терміналу", - "imageStorageTest": "Тест", - "imageStorageTesting": "Тестування...", - "imageStorageTestFailed": "Не вдалося перевірити сховище зображень", - "imageStorageTestConnected": "Термінальний сеанс підключено.", - "imageStorageTestNotConnected": "Немає підключеного термінального сеансу з цим ідентифікатором екземпляра.", - "imageStorageTestSelectedMode": "Для завантаження буде використано: {{mode}}", - "imageStorageSelectedModeUnavailable": "Недоступно", - "imageStorageTestLocalVisible": "Локальний каталог видно з підключеного хоста.", - "imageStorageTestLocalNotVisible": "Локальний каталог НЕ видно з підключеного хоста.", "sectionBranding": "Біла етикетка", "brandingAppName": "Назва програми", "brandingAppNameDesc": "Відображається на екрані входу та вкладці браузера. Залиште поле порожнім, щоб використовувати «Termix».", @@ -2354,9 +2241,6 @@ "oidcSilentLoginDefaultLockedDesc": "Цей параметр заблоковано змінною середовища OIDC_SILENT_LOGIN_DEFAULT і його не можна змінити тут.", "allowPasswordReset": "Дозволити скидання пароля", "allowPasswordResetDesc": "Скидання коду через журнали Docker", - "commandHistoryEnabled": "Історія команд", - "commandHistoryEnabledDesc": "Дозволити запис історії команд. Якщо вимкнено, історія не зберігається незалежно від налаштувань для кожного хоста.", - "updateCommandHistoryFailed": "Не вдалося оновити налаштування історії команд", "analyticsEnabled": "Поділитися анонімною статистикою використання", "analyticsEnabledDesc": "Надсилає анонімний щоденний підрахунок користувачів, хостів та використання функцій, щоб допомогти покращити Termix. Жодні особисті дані чи деталі з’єднання ніколи не включаються.", "analyticsEnabledLockedDesc": "Цей параметр заблоковано змінною середовища ENABLE_TELEMETRY і його не можна змінити тут.", @@ -2647,11 +2531,6 @@ "hostDefaultsTerminal": "Термінал", "hostDefaultsAutoTmux": "Auto-Tmux", "hostDefaultsAutoTmuxDesc": "Запускати термінали нових хостів всередині tmux, щоб сесії пережили відключення (потрібен tmux на хості)", - "terminalSessionTimeout": "Збереження термінального сеансу", - "terminalSessionTimeoutDesc": "Як довго відключений термінал залишається активним на сервері, перш ніж його буде закрито. Мін. 1 · Макс. 1440 хвилин. Для сеансів, які мають тривати довше цього часу, увімкніть Auto-Tmux на хості.", - "terminalSessionTimeoutRange": "Введіть значення від 1 до 1440 хвилин", - "terminalSessionTimeoutSaved": "Збережено збереження термінального сеансу", - "terminalSessionTimeoutSaveFailed": "Не вдалося зберегти дані про збереження термінального сеансу", "minutes": "хвилин", "hostDefaultsSessionLogging": "Ведення журналу сеансу", "hostDefaultsSessionLoggingDesc": "Запис термінальних сесій на нових хостах за замовчуванням", @@ -2708,15 +2587,6 @@ "connectToTerminal": "Підключення до терміналу", "connectToFiles": "Підключення до файлів" }, - "history": { - "noTerminalSelected": "Термінал не вибрано", - "noTerminalSelectedHint": "Відкрийте вкладку терміналу SSH, щоб переглянути історію його команд", - "searchPlaceholder": "Історія пошуку...", - "clearAll": "Очистити все", - "noHistoryEntries": "Немає записів в історії", - "trackingDisabled": "Відстеження історії вимкнено", - "trackingDisabledHint": "Увімкніть це в налаштуваннях терміналу хоста." - }, "sshTools": { "keyRecordingTitle": "Запис клавіш", "recordToTerminals": "Запис на термінали", @@ -3178,30 +3048,11 @@ "terminalToolbar": { "title": "Панель інструментів термінала", "showActions": "Показати панель інструментів", - "hideToolbar": "Приховати панель інструментів", - "showToolbar": "Розгорнути панель інструментів", - "moveToolbar": "Переміщення панелі інструментів", - "moveToolbarHint": "Перетягніть, щоб змінити положення панелі інструментів", - "layout": "Режим відображення панелі інструментів", - "layoutIcon": "Тільки значок", - "layoutLabeled": "Піктограми та мітки", - "layoutExpanded": "Розширено показниками", - "image": "Дії із зображеннями (лише зображення)", - "upload": "Завантажити зображення", - "paste": "Вставити зображення", - "detachTmux": "Від’єднати tmux", - "detachTmuxDescription": "Від’єднати сеанс tmux", - "imageActionFailed": "Не вдалося виконати дію із зображенням", - "retry": "Повторити спробу", - "dismissError": "Помилка відхилення зображення", "cpu": "Процесор", "memory": "Пам'ять", "disk": "Диск", "statsUnavailable": "Статистика в реальному часі недоступна", "tmuxDetach": "Від’єднати сеанс tmux", - "uploadImage": "Завантажте зображення на хостинг", - "uploadingImage": "Завантаження...", - "pasteImage": "Вставити зображення з буфера обміну (лише зображення)", "openFiles": "Відкрити файли", "openDocker": "Відкрити Докер", "openTunnel": "Відкритий тунель", @@ -3285,31 +3136,6 @@ "timeHours": "{{count}}год тому", "timeDays": "{{count}}днів тому" }, - "mobileKeyboard": { - "shift": "Зсув", - "ctrl": "Ctrl", - "esc": "Esc", - "tab": "Вкладка", - "backTab": "⇥", - "arrowUp": "Стрілка вгору", - "arrowDown": "Стрілка вниз", - "arrowLeft": "Стрілка ліворуч", - "arrowRight": "Стрілка праворуч", - "home": "Дім", - "end": "Кінець", - "pageUp": "PgUp", - "pageDown": "ПгДн", - "delete": "Дел", - "paste": "Вставити", - "editQuickKeys": "Редагувати швидкі клавіші", - "quickKeysTitle": "Швидкі клавіші", - "quickKeysDesc": "Натисніть ×, щоб видалити. Підтримує до 8 символів.", - "quickKeyPlaceholder": "наприклад, sudo ", - "addQuickKey": "Додати", - "removeQuickKey": "Видалити", - "resetDefaults": "Скинути до налаштувань за замовчуванням", - "done": "Готово" - }, "serial": { "title": "Серійний", "portLabel": "Порт", diff --git a/src/ui/locales/translated/vi_VN.json b/src/ui/locales/translated/vi_VN.json index a999dc5ac..40d13383a 100644 --- a/src/ui/locales/translated/vi_VN.json +++ b/src/ui/locales/translated/vi_VN.json @@ -742,7 +742,6 @@ "addHost": "Thêm máy chủ", "editHost": "Sửa máy chủ", "cloneHost": "Sao chép máy chủ", - "enableTerminal": "Bật Terminal", "enableTunnel": "Bật Tunnel", "enableFileManager": "Bật Trình Quản Lý Tệp", "enableDocker": "Bật Docker", @@ -1041,8 +1040,6 @@ "enableSessionLoggingDesc": "Ghi lại đầu ra phiên terminal để xem lại sau", "allowSessionSharing": "Cho phép chia sẻ phiên", "allowSessionSharingDesc": "Cho phép chia sẻ các phiên trực tiếp trên máy chủ này qua liên kết hoặc với những người dùng khác.", - "enableCommandHistory": "Lịch sử lệnh", - "enableCommandHistoryDesc": "Ghi lại các lệnh chạy trong terminal này để dùng cho lịch sử và tự động hoàn thành", "localEchoLabel": "Tiếng vọng địa phương", "localEchoDesc": "Dự đoán các thao tác gõ phím an toàn có thể in được cục bộ trên các kết nối chậm. Lời nhắc nhập mật khẩu và đầu vào điều khiển không bao giờ được hiển thị lại.", "localEchoDefault": "Sử dụng cài đặt toàn cầu", @@ -1072,8 +1069,6 @@ "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", - "enableTerminalToolbar": "Kích hoạt thanh công cụ Terminal", - "enableTerminalToolbarDesc": "Hiển thị thanh công cụ truy cập nhanh được gắn vào cửa sổ terminal đang hoạt động để xem các công cụ và số liệu thống kê trực tiếp của máy chủ này.", "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ụ.", @@ -1550,9 +1545,7 @@ "links": "Liên kết", "navigate": "Điều hướng", "select": "Chọn", - "toggleWith": "Chuyển đổi với", - "localTerminal": "Ga địa phương", - "localTerminalDesc": "Mở cửa sổ dòng lệnh trên máy tính này." + "toggleWith": "Chuyển đổi với" }, "splitScreen": { "paneEmpty": "Ngăn {{index}} - trống", @@ -1717,57 +1710,13 @@ "loadSharesFailed": "Không thể tải các chia sẻ đang hoạt động" }, "terminal": { - "sessionExpiredNotice": "Phiên làm việc trước của bạn trên {{host}} đã hết hạn sau {{minutes}} phút bị ngắt kết nối; shell và mọi thứ đang chạy trong đó đều đã biến mất. Hãy bật Auto-Tmux trên máy chủ này để duy trì các phiên làm việc.", - "sessionExpiredNoticeNoMinutes": "Phiên làm việc trước của bạn trên {{host}} đã hết hạn khi bị ngắt kết nối; shell và mọi thứ đang chạy trong đó đều đã biến mất. Hãy bật Auto-Tmux trên máy chủ này để duy trì các phiên làm việc.", - "enableAutoTmuxAction": "Bật Auto-Tmux", - "autoTmuxEnabled": "Tính năng Auto-Tmux đã được bật cho {{host}}. Tính năng này sẽ được áp dụng từ lần kết nối tiếp theo.", - "autoTmuxEnableFailed": "Không thể bật Auto-Tmux.", "connect": "Kết nối đến máy chủ", "clear": "Xóa", "paste": "Dán", - "reconnect": "Kết nối lại", - "connectionLost": "Mất kết nối", "connected": "Đã kết nối", "clipboardWriteFailed": "Sao chép vào clipboard thất bại. Đảm bảo trang được phục vụ qua HTTPS hoặc localhost.", "clipboardReadFailed": "Đọc từ clipboard thất bại. Đảm bảo quyền truy cập clipboard đã được cấp.", - "clipboardHttpWarning": "Không thể đọc nội dung clipboard. Một số trình duyệt hạn chế quyền truy cập clipboard khi không sử dụng HTTPS - hãy thử Ctrl+V hoặc sử dụng HTTPS để chạy Termix.", - "passwordPromptFillTitle": "Điền mật khẩu đã lưu vào dòng nhắc này?", - "unknownError": "Lỗi không xác định đã xảy ra", - "websocketError": "Lỗi kết nối WebSocket", - "connecting": "Đang kết nối...", - "noHostSelected": "Chưa chọn máy chủ", - "reconnecting": "Đang kết nối lại... ({{attempt}}/{{max}})", - "reconnected": "Đã kết nối lại thành công", - "tmuxSessionCreated": "Đã tạo phiên tmux: {{name}}", - "tmuxSessionAttached": "Đã gắn vào phiên tmux: {{name}}", - "tmuxUnavailable": "tmux chưa được cài đặt trên máy chủ từ xa, chuyển sang shell tiêu chuẩn", - "tmuxSessionPickerTitle": "Phiên tmux", - "tmuxSessionPickerDesc": "Các phiên tmux hiện có trên máy chủ này. Chọn một phiên để gắn lại hoặc tạo phiên mới.", - "tmuxWindows": "Cửa sổ", - "tmuxWindowCount": "{{count}} cửa sổ", - "tmuxAttached": "Các máy khách đã gắn kết", - "tmuxAttachedCount": "{{count}} đã gắn kết", - "tmuxLastActivity": "Hoạt động cuối", - "tmuxTimeJustNow": "vừa xong", - "tmuxTimeMinutes": "{{count}} phút trước", - "tmuxTimeHours": "{{count}} giờ trước", - "tmuxTimeDays": "{{count}} ngày trước", - "tmuxCreateNew": "Tạo phiên mới", - "tmuxCopyHint": "Điều chỉnh lựa chọn và nhấn Enter để sao chép vào clipboard", "tmuxDetach": "Ngắt khỏi phiên tmux", - "tmuxDetached": "Đã ngắt khỏi phiên tmux", - "searchPlaceholder": "Tìm thấy", - "searchCaseSensitive": "Hộp diêm", - "searchWholeWord": "Ghép toàn bộ từ", - "searchRegex": "Sử dụng biểu thức chính quy", - "searchNoResults": "Không có kết quả", - "searchResultCount": "{{index}} của {{count}}", - "searchNext": "Trận đấu tiếp theo (Nhập)", - "searchPrevious": "Kết quả khớp trước đó (Shift+Enter)", - "searchClose": "Đóng (Thoát)", - "maxReconnectAttemptsReached": "Đã đạt số lần kết nối lại tối đa", - "closeTab": "Đóng", - "connectionTimeout": "Hết thời gian kết nối", "terminalTitle": "Terminal - {{host}}", "terminalWithPath": "Terminal - {{host}}:{{path}}", "runTitle": "Đang chạy {{command}} - {{host}}", @@ -1790,27 +1739,14 @@ "opksshOpenBrowser": "Mở trình duyệt để xác thực", "opksshWaitingForAuth": "Đang chờ xác thực trên trình duyệt...", "opksshAuthenticating": "Đang xử lý xác thực...", - "opksshTimeout": "Xác thực đã hết thời gian chờ. Vui lòng thử lại.", - "opksshAuthFailed": "Xác thực thất bại. Vui lòng kiểm tra thông tin đăng nhập và thử lại.", "opksshSignInWith": "Đăng nhập bằng {{provider}}", - "vaultAuthTitle": "Yêu cầu đăng nhập Vault", - "vaultAuthDescription": "Một cửa sổ đã mở để đăng nhập vào HashiCorp Vault. Hoàn tất đăng nhập ở đó; kết nối này sẽ tự động tiếp tục.", - "vaultAuthFailed": "Xác thực Vault thất bại. Vui lòng thử lại.", - "vaultReopen": "Mở lại cửa sổ đăng nhập", "sudoPasswordPopupTitle": "Nhập mật khẩu?", - "linkDialogTitle": "Mở liên kết", - "linkDialogOpen": "Mở", - "linkDialogCopy": "Sao chép", - "websocketAbnormalClose": "Kết nối đóng đột ngột. Có thể do sự cố cấu hình reverse proxy hoặc SSL. Vui lòng kiểm tra nhật ký máy chủ.", "connectionLogTitle": "Nhật ký kết nối", "connectionLogCopy": "Sao chép nhật ký vào clipboard", "connectionLogEmpty": "Chưa có nhật ký kết nối", "connectionLogWaiting": "Đang chờ nhật ký kết nối...", "connectionLogCopied": "Đã sao chép nhật ký kết nối vào clipboard", "connectionLogCopyFailed": "Sao chép nhật ký vào clipboard thất bại", - "connectionRejected": "Kết nối bị máy chủ từ chối. Vui lòng kiểm tra xác thực và cấu hình mạng.", - "hostKeyRejected": "Xác minh khóa máy chủ SSH bị từ chối. Đã hủy kết nối.", - "sessionTakenOver": "Phiên đã được mở trong tab khác. Đang kết nối lại...", "split": { "splitTab": "Chia tab", "addToSplit": "Thêm vào phần chia", @@ -2225,55 +2161,6 @@ "sectionApiKeys": "Khóa API", "sectionAuditLog": "Nhật ký kiểm tra", "sectionSsl": "SSL / Let's Encrypt", - "sectionTouchInput": "Nhập liệu cảm ứng", - "touchEnabled": "Cho phép nhập liệu cảm ứng", - "touchEnabledDesc": "Cho phép cuộn bằng một ngón tay trong cửa sổ dòng lệnh.", - "touchMomentumEnabled": "Tạo đà", - "touchMomentumEnabledDesc": "Tiếp tục cuộn xuống một chút sau khi vuốt.", - "touchAdvanced": "Trình độ cao", - "touchDragThreshold": "Ngưỡng kéo", - "touchMaxWheelDelta": "Chênh lệch bánh xe tối đa", - "touchMomentumSampleWindow": "Cửa sổ mẫu động lượng", - "touchReleaseGracePeriod": "thời gian ân hạn thả", - "touchMinimumVelocity": "Vận tốc tối thiểu", - "touchMaximumVelocity": "Vận tốc tối đa", - "touchMaximumDuration": "Thời gian tối đa", - "touchMaximumTravel": "Hành trình tối đa", - "touchDecayTime": "Thời gian phân rã", - "touchPixelsPerTick": "Số pixel trên mỗi tick", - "touchMaximumFrameInterval": "Khoảng thời gian khung hình tối đa", - "touchMaximumTicksPerFrame": "Số tick tối đa mỗi khung hình", - "touchResetDefaults": "Khôi phục về cài đặt mặc định", - "touchSaved": "Cài đặt nhập liệu cảm ứng đã được lưu", - "touchSaveFailed": "Không thể lưu cài đặt nhập liệu cảm ứng.", - "sectionImageStorage": "Lưu trữ hình ảnh", - "imageStorageMode": "Chế độ lưu trữ", - "imageStorageModeDesc": "Nơi hiển thị hình ảnh từ thiết bị đầu cuối được dán và tải lên. Chức năng tự động chọn theo kết nối, cục bộ và từ xa (SFTP) được cố định.", - "imageStorageModeAuto": "Tự động", - "imageStorageModeLocal": "Địa phương", - "imageStorageModeRemoteSftp": "Từ xa (SFTP)", - "imageStorageLocalDir": "Thư mục cục bộ", - "imageStorageLocalDirDesc": "Đường dẫn tuyệt đối trên hệ thống quản trị của Termix. Không hiển thị sau khi lưu vì lý do bảo mật.", - "imageStorageLocalDirPlaceholder": "Để trống phần này.", - "imageStorageHostPath": "Đường dẫn máy chủ", - "imageStorageHostPathDesc": "Đây là giao diện phía máy chủ của thư mục cục bộ mà các tác nhân đầu cuối nhận được.", - "imageStorageTtl": "TTL (ms)", - "imageStorageMaxCount": "Số lượng ảnh tối đa", - "imageStorageMaxBytes": "Số byte lưu trữ tối đa", - "imageStorageSaved": "Cài đặt lưu trữ hình ảnh đã được lưu", - "imageStorageSaveFailed": "Không thể lưu cài đặt lưu trữ hình ảnh.", - "imageStorageInstanceId": "ID phiên bản phiên thiết bị đầu cuối", - "imageStorageInstanceIdDesc": "ID phiên bản của một trong các tab thiết bị đầu cuối được kết nối của bạn, được sử dụng để kiểm tra khả năng hiển thị.", - "imageStorageInstanceIdRequired": "Trước tiên, hãy nhập ID phiên bản thiết bị đầu cuối.", - "imageStorageTest": "Bài kiểm tra", - "imageStorageTesting": "Đang thử nghiệm...", - "imageStorageTestFailed": "Không thể kiểm tra lưu trữ hình ảnh", - "imageStorageTestConnected": "Phiên kết nối terminal đã được thiết lập.", - "imageStorageTestNotConnected": "Không có phiên làm việc thiết bị đầu cuối nào được kết nối với ID phiên bản này.", - "imageStorageTestSelectedMode": "Quá trình tải lên sẽ sử dụng: {{mode}}", - "imageStorageSelectedModeUnavailable": "Không khả dụng", - "imageStorageTestLocalVisible": "Thư mục cục bộ có thể được nhìn thấy từ máy chủ được kết nối.", - "imageStorageTestLocalNotVisible": "Thư mục cục bộ KHÔNG hiển thị từ máy chủ được kết nối.", "sectionBranding": "Nhãn trắng", "brandingAppName": "Tên ứng dụng", "brandingAppNameDesc": "Hiển thị trên màn hình đăng nhập và tab trình duyệt. Để trống để sử dụng \"Termix\".", @@ -2354,9 +2241,6 @@ "oidcSilentLoginDefaultLockedDesc": "Cài đặt này bị khóa bởi biến môi trường OIDC_SILENT_LOGIN_DEFAULT và không thể thay đổi ở đây.", "allowPasswordReset": "Cho phép đặt lại mật khẩu", "allowPasswordResetDesc": "Mã đặt lại qua nhật ký Docker", - "commandHistoryEnabled": "Lịch sử lệnh", - "commandHistoryEnabledDesc": "Cho phép ghi lịch sử lệnh. Khi tắt, lịch sử sẽ không được lưu bất kể cài đặt từng máy chủ.", - "updateCommandHistoryFailed": "Không thể cập nhật cài đặt lịch sử lệnh", "analyticsEnabled": "Chia sẻ số liệu thống kê sử dụng ẩn danh", "analyticsEnabledDesc": "Gửi báo cáo hàng ngày ẩn danh về số lượng người dùng, máy chủ và mức sử dụng tính năng để giúp cải thiện Termix. Không có dữ liệu cá nhân hoặc chi tiết kết nối nào được tiết lộ.", "analyticsEnabledLockedDesc": "Cài đặt này bị khóa bởi biến môi trường ENABLE_TELEMETRY và không thể thay đổi ở đây.", @@ -2647,11 +2531,6 @@ "hostDefaultsTerminal": "Terminal", "hostDefaultsAutoTmux": "Auto-Tmux", "hostDefaultsAutoTmuxDesc": "Chạy các cửa sổ terminal của máy chủ mới bên trong tmux để đảm bảo phiên làm việc vẫn hoạt động sau khi ngắt kết nối (cần cài đặt tmux trên máy chủ).", - "terminalSessionTimeout": "Duy trì phiên đầu cuối", - "terminalSessionTimeoutDesc": "Thời gian duy trì kết nối của một thiết bị đầu cuối bị ngắt kết nối trên máy chủ trước khi nó bị đóng. Tối thiểu 1 phút · Tối đa 1440 phút. Đối với các phiên cần duy trì lâu hơn thời gian này, hãy bật Auto-Tmux trên máy chủ.", - "terminalSessionTimeoutRange": "Nhập giá trị nằm trong khoảng từ 1 đến 1440 phút.", - "terminalSessionTimeoutSaved": "Lưu trữ phiên làm việc của thiết bị đầu cuối", - "terminalSessionTimeoutSaveFailed": "Không thể lưu trạng thái phiên làm việc của thiết bị đầu cuối", "minutes": "phút", "hostDefaultsSessionLogging": "Ghi phiên", "hostDefaultsSessionLoggingDesc": "Mặc định ghi lại phiên terminal trên máy chủ mới", @@ -2708,15 +2587,6 @@ "connectToTerminal": "Kết nối đến Terminal", "connectToFiles": "Kết nối đến Files" }, - "history": { - "noTerminalSelected": "Chưa chọn terminal", - "noTerminalSelectedHint": "Mở một tab terminal SSH để xem lịch sử lệnh", - "searchPlaceholder": "Tìm kiếm lịch sử...", - "clearAll": "Xóa tất cả", - "noHistoryEntries": "Không có mục lịch sử", - "trackingDisabled": "Tính năng ghi lịch sử đã bị tắt", - "trackingDisabledHint": "Bật tính năng này trong cài đặt terminal của máy chủ." - }, "sshTools": { "keyRecordingTitle": "Ghi phím", "recordToTerminals": "Ghi vào các terminal", @@ -3178,30 +3048,11 @@ "terminalToolbar": { "title": "Thanh công cụ thiết bị đầu cuối", "showActions": "Hiển thị thanh công cụ", - "hideToolbar": "Ẩn thanh công cụ", - "showToolbar": "Mở rộng thanh công cụ", - "moveToolbar": "Di chuyển thanh công cụ", - "moveToolbarHint": "Kéo để định vị lại thanh công cụ", - "layout": "Chế độ hiển thị thanh công cụ", - "layoutIcon": "Chỉ có biểu tượng", - "layoutLabeled": "Biểu tượng và nhãn", - "layoutExpanded": "Đã mở rộng với các số liệu", - "image": "Thao tác với hình ảnh (chỉ áp dụng cho hình ảnh)", - "upload": "Tải ảnh lên", - "paste": "Dán hình ảnh", - "detachTmux": "Tách tmux", - "detachTmuxDescription": "Tách phiên tmux", - "imageActionFailed": "Thao tác xử lý hình ảnh thất bại", - "retry": "Thử lại", - "dismissError": "Bỏ qua lỗi hình ảnh", "cpu": "CPU", "memory": "Ký ức", "disk": "Đĩa", "statsUnavailable": "Số liệu thống kê trực tiếp không khả dụng", "tmuxDetach": "Tách phiên tmux", - "uploadImage": "Tải ảnh lên máy chủ", - "uploadingImage": "Đang tải lên...", - "pasteImage": "Dán hình ảnh từ clipboard (chỉ áp dụng cho hình ảnh)", "openFiles": "Đang mở tệp", "openDocker": "Mở Docker", "openTunnel": "Đường hầm mở", @@ -3285,31 +3136,6 @@ "timeHours": "{{count}} giờ trước", "timeDays": "{{count}} ngày trước" }, - "mobileKeyboard": { - "shift": "Shift", - "ctrl": "Ctrl", - "esc": "Esc", - "tab": "Tab", - "backTab": "⇥", - "arrowUp": "Mũi tên lên", - "arrowDown": "Mũi tên xuống", - "arrowLeft": "Mũi tên trái", - "arrowRight": "Mũi tên phải", - "home": "Home", - "end": "End", - "pageUp": "PgUp", - "pageDown": "PgDn", - "delete": "Del", - "paste": "Dán", - "editQuickKeys": "Chỉnh sửa phím tắt", - "quickKeysTitle": "Phím tắt", - "quickKeysDesc": "Nhấn × để xóa. Hỗ trợ tối đa 8 ký tự.", - "quickKeyPlaceholder": "vd: sudo ", - "addQuickKey": "Thêm", - "removeQuickKey": "Xóa", - "resetDefaults": "Đặt lại mặc định", - "done": "Xong" - }, "serial": { "title": "Cổng nối tiếp", "portLabel": "Cổng", diff --git a/src/ui/locales/translated/zh_CN.json b/src/ui/locales/translated/zh_CN.json index f6848aedb..abfa89807 100644 --- a/src/ui/locales/translated/zh_CN.json +++ b/src/ui/locales/translated/zh_CN.json @@ -742,7 +742,6 @@ "addHost": "添加主机", "editHost": "编辑主机", "cloneHost": "克隆主机", - "enableTerminal": "启用终端", "enableTunnel": "启用隧道", "enableFileManager": "启用文件管理器", "enableDocker": "启用 Docker", @@ -1041,8 +1040,6 @@ "enableSessionLoggingDesc": "记录终端会话输出以供日后查看", "allowSessionSharing": "允许会话共享", "allowSessionSharingDesc": "允许通过链接或其他方式共享此主机上的实时会话。", - "enableCommandHistory": "命令历史", - "enableCommandHistoryDesc": "记录此终端中运行的命令,用于历史记录和自动补全", "localEchoLabel": "本地回声", "localEchoDesc": "在连接速度较慢的情况下,本地预测可安全打印的按键操作。密码提示和控制输入永远不会被回显。", "localEchoDefault": "使用全局设置", @@ -1072,8 +1069,6 @@ "bindHost": "绑定主机", "enableTmuxMonitor": "启用 Tmux 监控", "enableTmuxMonitorDesc": "在 Tmux 监控中显示此主机,并将其 tmux 操作添加到侧边栏", - "enableTerminalToolbar": "启用终端工具栏", - "enableTerminalToolbarDesc": "在当前终端上显示该主机的工具和实时信息的 Dock栏", "connectionToolbar": "连接工具栏", "showConnectionToolbar": "显示连接工具栏", "showConnectionToolbarDesc": "在主机的 SSH、RDP、VNC 和 Telnet 会话中显示工具栏。您也可以暂时将其从工具栏本身隐藏。", @@ -1550,9 +1545,7 @@ "links": "链接", "navigate": "导航", "select": "选择", - "toggleWith": "切换:使用", - "localTerminal": "本地终端", - "localTerminalDesc": "在这台计算机上打开一个 shell" + "toggleWith": "切换:使用" }, "splitScreen": { "paneEmpty": "窗格 {{index}} - 空", @@ -1717,57 +1710,13 @@ "loadSharesFailed": "无法加载共享" }, "terminal": { - "sessionExpiredNotice": "您之前在 {{host}} 上的会话已在 {{minutes}} 分钟后断开连接并过期;shell 及其中运行的所有内容均已丢失。请在此主机上启用 Auto-Tmux 以保持会话连接。", - "sessionExpiredNoticeNoMinutes": "您之前在 {{host}} 上的会话已过期,且已断开连接;shell 及其中运行的所有内容均已丢失。请在此主机上启用 Auto-Tmux 以保持会话有效。", - "enableAutoTmuxAction": "启用自动 Tmux", - "autoTmuxEnabled": "对 {{host}}启用自动 Tmux。它从下一个连接开始生效。", - "autoTmuxEnableFailed": "启用自动 Tmux 失败", "connect": "连接到主机", "clear": "清除", "paste": "粘贴", - "reconnect": "重新连接", - "connectionLost": "连接丢失", "connected": "已连接", "clipboardWriteFailed": "复制到剪贴板失败。请确保页面通过 HTTPS 或 localhost 提供服务。", "clipboardReadFailed": "从剪贴板读取失败。请确保已授予剪贴板权限。", - "clipboardHttpWarning": "无法读取剪贴板内容。部分浏览器限制在 HTTPS 协议之外访问剪贴板——请尝试使用 Ctrl+V 粘贴,或通过 HTTPS 协议访问 Termix。", - "passwordPromptFillTitle": "将保存的密码填入此提示?", - "unknownError": "发生未知错误", - "websocketError": "WebSocket 连接错误", - "connecting": "正在连接…", - "noHostSelected": "未选择主机", - "reconnecting": "正在重新连接…({{attempt}}/{{max}})", - "reconnected": "重新连接成功", - "tmuxSessionCreated": "tmux 会话已创建:{{name}}", - "tmuxSessionAttached": "tmux 会话已附加:{{name}}", - "tmuxUnavailable": "远程主机上未安装 tmux,回退到标准 shell", - "tmuxSessionPickerTitle": "tmux 会话", - "tmuxSessionPickerDesc": "在此主机上发现现有 tmux 会话。选择一个重新连接或创建新会话。", - "tmuxWindows": "窗口", - "tmuxWindowCount": "{{count}} 个窗口", - "tmuxAttached": "已连接客户端", - "tmuxAttachedCount": "{{count}} 个已连接", - "tmuxLastActivity": "最后活动", - "tmuxTimeJustNow": "刚刚", - "tmuxTimeMinutes": "{{count}} 分钟前", - "tmuxTimeHours": "{{count}} 小时前", - "tmuxTimeDays": "{{count}} 天前", - "tmuxCreateNew": "启动新会话", - "tmuxCopyHint": "调整选择并按 Enter 复制到剪贴板", "tmuxDetach": "从 tmux 会话分离", - "tmuxDetached": "已从 tmux 会话分离", - "searchPlaceholder": "搜索", - "searchCaseSensitive": "区分大小写", - "searchWholeWord": "全字匹配", - "searchRegex": "使用正则表达式", - "searchNoResults": "没有结果", - "searchResultCount": "{{index}} 的 {{count}}", - "searchNext": "下一个匹配(Enter)", - "searchPrevious": "上一个匹配(Shift+Enter)", - "searchClose": "关闭(Esc)", - "maxReconnectAttemptsReached": "已达到最大重连尝试次数", - "closeTab": "关闭", - "connectionTimeout": "连接超时", "terminalTitle": "终端 - {{host}}", "terminalWithPath": "终端 - {{host}}:{{path}}", "runTitle": "运行 {{command}} - {{host}}", @@ -1790,27 +1739,14 @@ "opksshOpenBrowser": "打开浏览器进行认证", "opksshWaitingForAuth": "等待浏览器认证...", "opksshAuthenticating": "正在处理认证...", - "opksshTimeout": "认证超时,请重试。", - "opksshAuthFailed": "认证失败,请检查凭据并重试。", "opksshSignInWith": "使用 {{provider}} 登录", - "vaultAuthTitle": "需要 Vault 登录", - "vaultAuthDescription": "已打开窗口以登录 HashiCorp Vault。在该处完成登录;此连接将自动继续。", - "vaultAuthFailed": "Vault 认证失败,请重试。", - "vaultReopen": "重新打开登录窗口", "sudoPasswordPopupTitle": "输入密码?", - "linkDialogTitle": "打开链接", - "linkDialogOpen": "打开", - "linkDialogCopy": "复制", - "websocketAbnormalClose": "连接意外关闭。这可能是因为反向代理或SSL配置问题。请检查服务器日志。", "connectionLogTitle": "连接日志", "connectionLogCopy": "复制日志到剪贴板", "connectionLogEmpty": "暂无连接日志", "connectionLogWaiting": "等待连接日志...", "connectionLogCopied": "连接日志已复制到剪贴板", "connectionLogCopyFailed": "复制日志到剪贴板失败", - "connectionRejected": "服务器拒绝了连接。请检查您的身份验证和网络配置。", - "hostKeyRejected": "SSH主机密钥验证被拒绝。连接已取消。", - "sessionTakenOver": "会话已在另一个标签页中打开。正在重新连接...", "split": { "splitTab": "拆分标签页", "addToSplit": "添加到拆分", @@ -2225,55 +2161,6 @@ "sectionApiKeys": "API 密钥", "sectionAuditLog": "审计日志", "sectionSsl": "SSL / Let's Encrypt", - "sectionTouchInput": "触摸输入", - "touchEnabled": "启用触摸输入", - "touchEnabledDesc": "允许终端单指触摸滚动", - "touchMomentumEnabled": "启用动能", - "touchMomentumEnabledDesc": "滑动后继续短暂滚动。", - "touchAdvanced": "高级", - "touchDragThreshold": "拖动阈值", - "touchMaxWheelDelta": "最大滚轮增量", - "touchMomentumSampleWindow": "动量样本窗口", - "touchReleaseGracePeriod": "释放宽限期", - "touchMinimumVelocity": "最小速度", - "touchMaximumVelocity": "最大速度", - "touchMaximumDuration": "最长持续时间", - "touchMaximumTravel": "最大行程", - "touchDecayTime": "衰减时间", - "touchPixelsPerTick": "每刻像素数", - "touchMaximumFrameInterval": "最大帧间隔", - "touchMaximumTicksPerFrame": "每帧最大刻度数", - "touchResetDefaults": "恢复默认设置", - "touchSaved": "已保存触摸输入设置", - "touchSaveFailed": "触摸输入设置保存失败", - "sectionImageStorage": "图像存储", - "imageStorageMode": "存储模式", - "imageStorageModeDesc": "粘贴和上传的终端图片的写入位置。自动模式根据连接自动选择,本地和远程(SFTP)为固定模式。", - "imageStorageModeAuto": "自动", - "imageStorageModeLocal": "本地", - "imageStorageModeRemoteSftp": "远程(SFTP)", - "imageStorageLocalDir": "本地目录", - "imageStorageLocalDirDesc": "Termix 后端上的绝对路径。出于安全考虑,保存后将不显示。", - "imageStorageLocalDirPlaceholder": "留空以保持当前状态", - "imageStorageHostPath": "主机路径", - "imageStorageHostPathDesc": "终端代理接收到的本地目录的主机端视图。", - "imageStorageTtl": "TTL(毫秒)", - "imageStorageMaxCount": "最大图像数量", - "imageStorageMaxBytes": "最大存储字节", - "imageStorageSaved": "已保存图像存储设置", - "imageStorageSaveFailed": "图像存储设置保存失败", - "imageStorageInstanceId": "终端会话实例 ID", - "imageStorageInstanceIdDesc": "用于测试可见性的已连接终端标签页的实例 ID。", - "imageStorageInstanceIdRequired": "请先输入终端会话实例 ID", - "imageStorageTest": "测试", - "imageStorageTesting": "测试中……", - "imageStorageTestFailed": "图像存储测试失败", - "imageStorageTestConnected": "终端会话已连接。", - "imageStorageTestNotConnected": "没有与此实例 ID 连接的终端会话。", - "imageStorageTestSelectedMode": "上传将使用:{{mode}}", - "imageStorageSelectedModeUnavailable": "不可用", - "imageStorageTestLocalVisible": "本地目录对连接的主机可见。", - "imageStorageTestLocalNotVisible": "从连接的主机上不可见本地目录。", "sectionBranding": "白标", "brandingAppName": "应用程序名称", "brandingAppNameDesc": "显示在登录界面和浏览器标签页上。留空则使用“Termix”。", @@ -2354,9 +2241,6 @@ "oidcSilentLoginDefaultLockedDesc": "此设置受 OIDC_SILENT_LOGIN_DEFAULT 环境变量锁定,无法在此处更改。", "allowPasswordReset": "允许密码重置", "allowPasswordResetDesc": "通过 Docker 日志获取重置码", - "commandHistoryEnabled": "命令历史", - "commandHistoryEnabledDesc": "允许记录命令历史。禁用后,无论各主机设置如何,均不保存历史记录。", - "updateCommandHistoryFailed": "更新命令历史设置失败", "analyticsEnabled": "共享匿名使用统计数据", "analyticsEnabledDesc": "每日匿名发送用户、主机和功能使用情况统计数据,以帮助改进 Termix。绝不包含任何个人数据或连接详情。", "analyticsEnabledLockedDesc": "此设置受 ENABLE_TELEMETRY 环境变量锁定,无法在此处更改。", @@ -2647,11 +2531,6 @@ "hostDefaultsTerminal": "终端", "hostDefaultsAutoTmux": "自动 Tmux", "hostDefaultsAutoTmuxDesc": "在 tmux 中运行新主机的终端,以便会话在断开连接后仍然存在(主机上需要安装 tmux)。", - "terminalSessionTimeout": "终端会话持久性", - "terminalSessionTimeoutDesc": "断开连接的终端在服务器上保持活动状态的最长时间。最短 1 分钟,最长 1440 分钟。对于必须保持活动时间超过此限制的会话,请在主机上启用自动终端复用 (Auto-Tmux)。", - "terminalSessionTimeoutRange": "请输入 1 到 1440 分钟之间的数值", - "terminalSessionTimeoutSaved": "终端会话持久性已保存", - "terminalSessionTimeoutSaveFailed": "保存终端会话持久性失败", "minutes": "分钟", "hostDefaultsSessionLogging": "会话记录", "hostDefaultsSessionLoggingDesc": "默认记录新主机的终端会话", @@ -2708,15 +2587,6 @@ "connectToTerminal": "连接到终端", "connectToFiles": "连接到文件管理器" }, - "history": { - "noTerminalSelected": "未选择终端", - "noTerminalSelectedHint": "打开一个 SSH 终端标签页以查看其命令历史", - "searchPlaceholder": "搜索历史...", - "clearAll": "全部清除", - "noHistoryEntries": "无历史记录", - "trackingDisabled": "历史跟踪已禁用", - "trackingDisabledHint": "在主机终端设置中启用。" - }, "sshTools": { "keyRecordingTitle": "按键录制", "recordToTerminals": "录制到终端", @@ -3178,30 +3048,11 @@ "terminalToolbar": { "title": "终端工具栏", "showActions": "显示工具栏", - "hideToolbar": "隐藏工具栏", - "showToolbar": "展开工具栏", - "moveToolbar": "移动工具栏", - "moveToolbarHint": "拖动即可重新定位工具栏", - "layout": "工具栏显示模式", - "layoutIcon": "仅图标", - "layoutLabeled": "图标和标签", - "layoutExpanded": "扩展了指标", - "image": "图片操作(仅限图片)", - "upload": "上传图片", - "paste": "粘贴图片", - "detachTmux": "分离 tmux", - "detachTmuxDescription": "分离 tmux 会话", - "imageActionFailed": "图片操作失败", - "retry": "重试", - "dismissError": "忽略图像错误", "cpu": "CPU", "memory": "内存", "disk": "磁盘", "statsUnavailable": "实时统计数据不可用", "tmuxDetach": "分离 tmux 会话", - "uploadImage": "将图片上传到主机", - "uploadingImage": "正在上传...", - "pasteImage": "从剪贴板粘贴图片(仅限图片)", "openFiles": "打开文件", "openDocker": "Open Docker", "openTunnel": "打开隧道", @@ -3285,31 +3136,6 @@ "timeHours": "{{count}} 小时前", "timeDays": "{{count}} 天前" }, - "mobileKeyboard": { - "shift": "Shift", - "ctrl": "Ctrl", - "esc": "Esc", - "tab": "Tab", - "backTab": "⇥", - "arrowUp": "Arrow Up", - "arrowDown": "Arrow Down", - "arrowLeft": "Arrow Left", - "arrowRight": "Arrow Right", - "home": "Home", - "end": "End", - "pageUp": "PgUp", - "pageDown": "PgDn", - "delete": "Del", - "paste": "粘贴", - "editQuickKeys": "编辑快速键", - "quickKeysTitle": "快速键", - "quickKeysDesc": "点击 × 删除。最多支持 8 个字符。", - "quickKeyPlaceholder": "例如 sudo ", - "addQuickKey": "添加", - "removeQuickKey": "删除", - "resetDefaults": "重置为默认值", - "done": "完成" - }, "serial": { "title": "串口", "portLabel": "端口", diff --git a/src/ui/locales/translated/zh_TW.json b/src/ui/locales/translated/zh_TW.json index edff754e2..031e06872 100644 --- a/src/ui/locales/translated/zh_TW.json +++ b/src/ui/locales/translated/zh_TW.json @@ -742,7 +742,6 @@ "addHost": "新增主機", "editHost": "編輯主機", "cloneHost": "複製主機", - "enableTerminal": "啟用終端機", "enableTunnel": "啟用通道", "enableFileManager": "啟用檔案管理員", "enableDocker": "啟用 Docker", @@ -1041,8 +1040,6 @@ "enableSessionLoggingDesc": "記錄終端機工作階段輸出以供日後檢視", "allowSessionSharing": "允許會話共享", "allowSessionSharingDesc": "允許透過連結或其他方式分享此主機上的即時會話。", - "enableCommandHistory": "命令歷史記錄", - "enableCommandHistoryDesc": "記錄在此終端機中執行的命令,用於歷史記錄與自動完成", "localEchoLabel": "本地迴聲", "localEchoDesc": "在連線速度較慢的情況下,本地預測可安全列印的按鍵操作。密碼提示和控制輸入永遠不會被回顯。", "localEchoDefault": "使用全域設定", @@ -1072,8 +1069,6 @@ "bindHost": "綁定主機", "enableTmuxMonitor": "啟用 Tmux 監控器", "enableTmuxMonitorDesc": "在 Tmux 監控器中顯示此主機,並將其 tmux 操作新增至側邊欄", - "enableTerminalToolbar": "啟用終端機工具列", - "enableTerminalToolbarDesc": "在目前終端上顯示一個快速存取工具列,用於存取該主機的工具和即時統計資訊。", "connectionToolbar": "連接工具列", "showConnectionToolbar": "顯示連接工具列", "showConnectionToolbarDesc": "在主機的 SSH、RDP、VNC 和 Telnet 會話中顯示工具列。您也可以暫時將其從工具列本身隱藏。", @@ -1550,9 +1545,7 @@ "links": "連結", "navigate": "導覽", "select": "選取", - "toggleWith": "切換方式", - "localTerminal": "本地端", - "localTerminalDesc": "在這台電腦上開啟一個 shell" + "toggleWith": "切換方式" }, "splitScreen": { "paneEmpty": "窗格 {{index}} - 空白", @@ -1717,57 +1710,13 @@ "loadSharesFailed": "無法加載活動份額" }, "terminal": { - "sessionExpiredNotice": "您之前在 {{host}} 上的會話已在 {{minutes}} 分鐘後斷開連接並過期;shell 及其中運行的所有內容均已丟失。請在此主機上啟用 Auto-Tmux 以保持會話連線。", - "sessionExpiredNoticeNoMinutes": "您先前在 {{host}} 上的會話已過期,且已斷線;shell 及其中執行的所有內容均已遺失。請在此主機上啟用 Auto-Tmux 以保持會話有效。", - "enableAutoTmuxAction": "啟用自動 Tmux", - "autoTmuxEnabled": "對 {{host}}啟用自動 Tmux。它從下一個連接開始生效。", - "autoTmuxEnableFailed": "啟用自動 Tmux 失敗", "connect": "連線至主機", "clear": "清除", "paste": "貼上", - "reconnect": "重新連線", - "connectionLost": "連線中斷", "connected": "已連線", "clipboardWriteFailed": "複製到剪貼簿失敗。請確認頁面是透過 HTTPS 或 localhost 提供。", "clipboardReadFailed": "從剪貼簿讀取失敗。請確認已授予剪貼簿權限。", - "clipboardHttpWarning": "無法讀取剪貼簿內容。部分瀏覽器限制在 HTTPS 協定之外存取剪貼簿內容 - 請嘗試使用 Ctrl+V 貼上,或透過 HTTPS 協定存取 Termix。", - "passwordPromptFillTitle": "將已儲存的密碼填入此提示?", - "unknownError": "發生未知錯誤", - "websocketError": "WebSocket 連線錯誤", - "connecting": "連線中...", - "noHostSelected": "未選取主機", - "reconnecting": "重新連線中... ({{attempt}}/{{max}})", - "reconnected": "已成功重新連線", - "tmuxSessionCreated": "tmux 工作階段已建立:{{name}}", - "tmuxSessionAttached": "tmux 工作階段已附加:{{name}}", - "tmuxUnavailable": "遠端主機未安裝 tmux,切換回標準 shell", - "tmuxSessionPickerTitle": "tmux 工作階段", - "tmuxSessionPickerDesc": "在此主機上找到現有的 tmux 工作階段。選擇一個以重新附加或建立新的工作階段。", - "tmuxWindows": "視窗", - "tmuxWindowCount": "{{count}} 個視窗", - "tmuxAttached": "已附加的用戶端", - "tmuxAttachedCount": "{{count}} 個已附加", - "tmuxLastActivity": "上次活動", - "tmuxTimeJustNow": "剛剛", - "tmuxTimeMinutes": "{{count}} 分鐘前", - "tmuxTimeHours": "{{count}} 小時前", - "tmuxTimeDays": "{{count}} 天前", - "tmuxCreateNew": "開始新的工作階段", - "tmuxCopyHint": "調整選取範圍並按下 Enter 以複製到剪貼簿", "tmuxDetach": "從 tmux 工作階段中斷連線", - "tmuxDetached": "已從 tmux 工作階段中斷連線", - "searchPlaceholder": "尋找", - "searchCaseSensitive": "火柴盒", - "searchWholeWord": "整詞匹配", - "searchRegex": "使用正規表示式", - "searchNoResults": "沒有結果", - "searchResultCount": "{{index}} 的 {{count}}", - "searchNext": "下一場比賽(輸入)", - "searchPrevious": "最後一場比賽(Shift+Enter)", - "searchClose": "關閉(退出)", - "maxReconnectAttemptsReached": "已達最大重新連線嘗試次數", - "closeTab": "關閉", - "connectionTimeout": "連線逾時", "terminalTitle": "終端機 - {{host}}", "terminalWithPath": "終端機 - {{host}}:{{path}}", "runTitle": "正在執行 {{command}} - {{host}}", @@ -1790,27 +1739,14 @@ "opksshOpenBrowser": "開啟瀏覽器以進行驗證", "opksshWaitingForAuth": "正在等待瀏覽器中的驗證…", "opksshAuthenticating": "正在處理驗證…", - "opksshTimeout": "驗證逾時。請再試一次。", - "opksshAuthFailed": "驗證失敗。請檢查您的憑證並再試一次。", "opksshSignInWith": "使用 {{provider}} 登入", - "vaultAuthTitle": "需要 Vault 登入", - "vaultAuthDescription": "已開啟視窗以登入 HashiCorp Vault。在該處完成登入;此連線將自動繼續。", - "vaultAuthFailed": "Vault 驗證失敗。請再試一次。", - "vaultReopen": "重新開啟登入視窗", "sudoPasswordPopupTitle": "輸入密碼?", - "linkDialogTitle": "開啟連結", - "linkDialogOpen": "開啟", - "linkDialogCopy": "複製", - "websocketAbnormalClose": "連線意外關閉。這可能是由於反向代理或 SSL 設定問題所導致。請檢查伺服器記錄。", "connectionLogTitle": "連線記錄", "connectionLogCopy": "複製記錄到剪貼簿", "connectionLogEmpty": "尚無連線記錄", "connectionLogWaiting": "正在等待連線記錄…", "connectionLogCopied": "連線記錄已複製到剪貼簿", "connectionLogCopyFailed": "無法將連線記錄複製到剪貼簿", - "connectionRejected": "連線被伺服器拒絕。請檢查您的驗證與網路設定。", - "hostKeyRejected": "SSH 主機金鑰驗證被拒絕。連線已取消。", - "sessionTakenOver": "工作階段已在另一個分頁中開啟。正在重新連線…", "split": { "splitTab": "分割分頁", "addToSplit": "加入分割", @@ -2225,55 +2161,6 @@ "sectionApiKeys": "API 金鑰", "sectionAuditLog": "稽核記錄", "sectionSsl": "SSL / Let's Encrypt", - "sectionTouchInput": "觸摸輸入", - "touchEnabled": "啟用觸控輸入", - "touchEnabledDesc": "允許終端單指觸摸滾動", - "touchMomentumEnabled": "啟用動能", - "touchMomentumEnabledDesc": "滑動後繼續短暫滾動。", - "touchAdvanced": "先進的", - "touchDragThreshold": "阻力閾值", - "touchMaxWheelDelta": "最大車輪增量", - "touchMomentumSampleWindow": "動量樣本窗口", - "touchReleaseGracePeriod": "發布寬限期", - "touchMinimumVelocity": "最低速度", - "touchMaximumVelocity": "最大速度", - "touchMaximumDuration": "最長持續時間", - "touchMaximumTravel": "最大行程", - "touchDecayTime": "衰減時間", - "touchPixelsPerTick": "每刻像素數", - "touchMaximumFrameInterval": "最大訊框間隔", - "touchMaximumTicksPerFrame": "每格最大幀數", - "touchResetDefaults": "恢復預設設定", - "touchSaved": "已儲存觸控輸入設定", - "touchSaveFailed": "觸控輸入設定儲存失敗", - "sectionImageStorage": "影像儲存", - "imageStorageMode": "儲存模式", - "imageStorageModeDesc": "終端鏡像貼上和上傳位置已設定。自動選擇連接、本機和遠端(SFTP)鏡像的功能已修復。", - "imageStorageModeAuto": "汽車", - "imageStorageModeLocal": "當地的", - "imageStorageModeRemoteSftp": "遠端(SFTP)", - "imageStorageLocalDir": "本地目錄", - "imageStorageLocalDirDesc": "Termix 後端上的絕對路徑。出於安全考慮,儲存後將不顯示。", - "imageStorageLocalDirPlaceholder": "留空以保持當前狀態", - "imageStorageHostPath": "主機路徑", - "imageStorageHostPathDesc": "終端代理接收到的本機目錄的主機端視圖。", - "imageStorageTtl": "TTL(毫秒)", - "imageStorageMaxCount": "最大影像數量", - "imageStorageMaxBytes": "最大儲存位元組", - "imageStorageSaved": "已儲存影像儲存設定", - "imageStorageSaveFailed": "影像儲存設定保存失敗", - "imageStorageInstanceId": "終端機會話實例 ID", - "imageStorageInstanceIdDesc": "用於測試可見性的已連接終端選項卡的實例 ID。", - "imageStorageInstanceIdRequired": "請先輸入終端機會話實例 ID", - "imageStorageTest": "測試", - "imageStorageTesting": "測試中…", - "imageStorageTestFailed": "影像儲存測試失敗", - "imageStorageTestConnected": "終端會話已連線。", - "imageStorageTestNotConnected": "沒有與此實例 ID 連線的終端會話。", - "imageStorageTestSelectedMode": "上傳時請使用: {{mode}}", - "imageStorageSelectedModeUnavailable": "不可用", - "imageStorageTestLocalVisible": "本機目錄對連接的主機可見。", - "imageStorageTestLocalNotVisible": "從連線的主機上不可見本機目錄。", "sectionBranding": "白標", "brandingAppName": "應用程式名稱", "brandingAppNameDesc": "顯示在登入介面和瀏覽器標籤頁上。留空則使用“Termix”。", @@ -2354,9 +2241,6 @@ "oidcSilentLoginDefaultLockedDesc": "此設定受 OIDC_SILENT_LOGIN_DEFAULT 環境變數鎖定,無法在此變更。", "allowPasswordReset": "允許密碼重設", "allowPasswordResetDesc": "透過 Docker 日誌取得重設代碼", - "commandHistoryEnabled": "指令歷史記錄", - "commandHistoryEnabledDesc": "允許記錄指令歷史記錄。停用時,無論各主機設定如何,都不會儲存歷史記錄。", - "updateCommandHistoryFailed": "無法更新指令歷史記錄設定", "analyticsEnabled": "分享匿名使用統計數據", "analyticsEnabledDesc": "每日匿名傳送使用者、主機和功能使用統計數據,以協助改善 Termix。絕不包含任何個人資料或連結詳情。", "analyticsEnabledLockedDesc": "此設定受 ENABLE_TELEMETRY 環境變數鎖定,無法在此變更。", @@ -2647,11 +2531,6 @@ "hostDefaultsTerminal": "終端機", "hostDefaultsAutoTmux": "自動 Tmux", "hostDefaultsAutoTmuxDesc": "在 tmux 中執行新主機的終端,以便在斷開連線後會話仍然存在(主機上需要安裝 tmux)。", - "terminalSessionTimeout": "終端會話持久性", - "terminalSessionTimeoutDesc": "斷開連線的終端機在伺服器上保持活動狀態的最長時間。最短 1 分鐘,最長 1440 分鐘。對於必須保持活動時間超過此限制的會話,請在主機上啟用自動終端機重複使用 (Auto-Tmux)。", - "terminalSessionTimeoutRange": "請輸入 1 到 1440 分鐘之間的數值", - "terminalSessionTimeoutSaved": "終端機會話持久性已保存", - "terminalSessionTimeoutSaveFailed": "保存終端機會話持久性失敗", "minutes": "分分鐘", "hostDefaultsSessionLogging": "工作階段記錄", "hostDefaultsSessionLoggingDesc": "預設在新主機上記錄終端機工作階段", @@ -2708,15 +2587,6 @@ "connectToTerminal": "連線至終端機", "connectToFiles": "連線至檔案管理員" }, - "history": { - "noTerminalSelected": "未選取終端機", - "noTerminalSelectedHint": "開啟 SSH 終端機分頁以檢視其命令歷史記錄", - "searchPlaceholder": "搜尋歷史記錄...", - "clearAll": "全部清除", - "noHistoryEntries": "無歷史記錄項目", - "trackingDisabled": "歷史記錄追蹤已停用", - "trackingDisabledHint": "在主機的終端機設定中啟用。" - }, "sshTools": { "keyRecordingTitle": "按鍵錄製", "recordToTerminals": "錄製至終端機", @@ -3178,30 +3048,11 @@ "terminalToolbar": { "title": "終端機工具列", "showActions": "顯示工具列", - "hideToolbar": "隱藏工具列", - "showToolbar": "展開工具列", - "moveToolbar": "移動工具列", - "moveToolbarHint": "拖曳即可重新定位工具列", - "layout": "工具列顯示模式", - "layoutIcon": "僅圖示", - "layoutLabeled": "圖標和標籤", - "layoutExpanded": "擴展了指標", - "image": "圖片操作(僅限圖片)", - "upload": "上傳圖片", - "paste": "貼上圖片", - "detachTmux": "分離 tmux", - "detachTmuxDescription": "分離 tmux 會話", - "imageActionFailed": "圖片操作失敗", - "retry": "重試", - "dismissError": "忽略圖像錯誤", "cpu": "中央處理器", "memory": "記憶", "disk": "磁碟", "statsUnavailable": "即時統計數據不可用", "tmuxDetach": "分離 tmux 會話", - "uploadImage": "將圖片上傳到主機", - "uploadingImage": "正在上傳...", - "pasteImage": "從剪貼簿貼上圖片(僅限圖片)", "openFiles": "開啟文件", "openDocker": "Open Docker", "openTunnel": "開放式隧道", @@ -3285,31 +3136,6 @@ "timeHours": "{{count}} 小時前", "timeDays": "{{count}} 天前" }, - "mobileKeyboard": { - "shift": "Shift", - "ctrl": "Ctrl", - "esc": "Esc", - "tab": "Tab", - "backTab": "⇥", - "arrowUp": "上箭頭", - "arrowDown": "下箭頭", - "arrowLeft": "左箭頭", - "arrowRight": "右箭頭", - "home": "Home", - "end": "End", - "pageUp": "PgUp", - "pageDown": "PgDn", - "delete": "Del", - "paste": "貼上", - "editQuickKeys": "編輯快捷鍵", - "quickKeysTitle": "快捷鍵", - "quickKeysDesc": "點選 × 來移除。最多支援 8 個字元。", - "quickKeyPlaceholder": "例如:sudo ", - "addQuickKey": "新增", - "removeQuickKey": "移除", - "resetDefaults": "重設為預設值", - "done": "完成" - }, "serial": { "title": "序列", "portLabel": "連接埠", diff --git a/src/ui/main-axios.ts b/src/ui/main-axios.ts index a516afdc2..95bf93849 100644 --- a/src/ui/main-axios.ts +++ b/src/ui/main-axios.ts @@ -1960,8 +1960,6 @@ export { updatePasswordResetAllowed, updateOIDCConfig, disableOIDCConfig, - getCommandHistoryEnabled, - updateCommandHistoryEnabled, adminResetUserPassword, adminDisableUserTotp, adminExportUserData, @@ -2070,13 +2068,6 @@ export { } from "@/api/dashboard-api"; // ============================================================================ -export { - saveCommandToHistory, - getCommandHistory, - deleteCommandFromHistory, - clearCommandHistory, -} from "@/api/command-history-api"; - export { linkOIDCToPasswordAccount, unlinkOIDCFromPasswordAccount, diff --git a/src/ui/onboarding/steps/WelcomeStep.tsx b/src/ui/onboarding/steps/WelcomeStep.tsx index 2ae3163c0..0e10ff0c0 100644 --- a/src/ui/onboarding/steps/WelcomeStep.tsx +++ b/src/ui/onboarding/steps/WelcomeStep.tsx @@ -2,9 +2,21 @@ import { useTranslation } from "react-i18next"; import { FolderTree, Server, TerminalSquare } from "lucide-react"; const HIGHLIGHTS = [ - { icon: Server, key: "hosts" }, - { icon: TerminalSquare, key: "terminal" }, - { icon: FolderTree, key: "files" }, + { + icon: Server, + titleKey: "onboarding.welcome_hosts", + descKey: "onboarding.welcome_hosts_desc", + }, + { + icon: TerminalSquare, + titleKey: "onboarding.welcome_terminal", + descKey: "onboarding.welcome_terminal_desc", + }, + { + icon: FolderTree, + titleKey: "onboarding.welcome_files", + descKey: "onboarding.welcome_files_desc", + }, ] as const; export function WelcomeStep() { @@ -17,18 +29,16 @@ export function WelcomeStep() { </p> <div className="flex flex-col gap-1.5"> - {HIGHLIGHTS.map(({ icon: Icon, key }) => ( + {HIGHLIGHTS.map(({ icon: Icon, titleKey, descKey }) => ( <div - key={key} + key={titleKey} className="flex items-start gap-2.5 border border-border bg-card p-2.5" > <Icon size={14} className="mt-0.5 shrink-0 text-accent-brand" /> <div className="flex flex-col gap-0.5"> - <span className="text-xs font-medium"> - {t(`onboarding.welcome_${key}`)} - </span> + <span className="text-xs font-medium">{t(titleKey)}</span> <span className="text-[10px] leading-snug text-muted-foreground"> - {t(`onboarding.welcome_${key}_desc`)} + {t(descKey)} </span> </div> </div> diff --git a/src/ui/plugin-host/app.ts b/src/ui/plugin-host/app.ts index 4e2f9014d..cada5ff88 100644 --- a/src/ui/plugin-host/app.ts +++ b/src/ui/plugin-host/app.ts @@ -31,6 +31,7 @@ import { registerSlotContribution, } from "@/shell/action-registry"; import { pluginWsUrl } from "@/lib/plugin-transport"; +import { registerPluginComponent } from "./component-registry"; import type { WidgetTypeDefinition } from "@/types/homepage-types"; import type { LucideIcon } from "lucide-react"; import { @@ -168,6 +169,9 @@ export function createPluginApp( standaloneViews: options.standaloneViews, panelFrame: options.panelFrame, inLayouts: options.inLayouts, + commandTarget: options.commandTarget, + ownBackground: options.ownBackground, + multiInstance: options.multiInstance, preload: options.preload, }), ); @@ -305,6 +309,10 @@ export function createPluginApp( invokeAction: (id, ...args) => invokeAction(id, ...args), + registerComponent(id, component) { + return track(registerPluginComponent(id, scoped(component))); + }, + registerSshAuthEditor(editor) { return track( registerSshAuthEditor({ diff --git a/src/ui/plugin-host/bridge.ts b/src/ui/plugin-host/bridge.ts index dc120c4f9..b132da05c 100644 --- a/src/ui/plugin-host/bridge.ts +++ b/src/ui/plugin-host/bridge.ts @@ -26,6 +26,7 @@ import { tabsApi, useShellHosts } from "./shell-bridge"; import { invokeAction } from "@/shell/action-registry"; import { useSshAuthProviders } from "@/hooks/useSshAuthProviders"; import { useActionSlot } from "@/hooks/use-action-slot"; +import { usePluginComponent } from "./component-registry"; /** * Core permission groups, mirroring RESERVED_PERMISSION_PREFIXES in the SDK @@ -272,7 +273,9 @@ export const pluginHostBridge: PluginHostBridge = { }; }, - useSlotContributions: (slotId) => useActionSlot(slotId), + useSlotContributions: (slotId, context) => useActionSlot(slotId, context), + + usePluginComponent: (id) => usePluginComponent(id), }; /** Installs the bridge. Idempotent. */ diff --git a/src/ui/plugin-host/component-registry.ts b/src/ui/plugin-host/component-registry.ts new file mode 100644 index 000000000..3ae6ff76a --- /dev/null +++ b/src/ui/plugin-host/component-registry.ts @@ -0,0 +1,80 @@ +import { + createElement, + useSyncExternalStore, + type ComponentType, + type ReactNode, +} from "react"; + +/** + * Components a plugin offers other code by id, through app.registerComponent. + * + * The owner decides what the component is and the caller decides where it + * goes: the ssh-terminal plugin offers "terminal.view", and the collab room, + * the homepage SSH widget and the file manager render it without importing + * the terminal. While the owner is off the id resolves to nothing and the + * caller's fallback shows instead. + */ + +type AnyComponent = ComponentType<Record<string, unknown>>; + +const components = new Map<string, AnyComponent>(); +const listeners = new Set<() => void>(); +let version = 0; + +function notify(): void { + version += 1; + for (const listener of listeners) listener(); +} + +export function registerPluginComponent( + id: string, + component: AnyComponent, +): () => void { + components.set(id, component); + notify(); + return () => { + if (components.get(id) === component) { + components.delete(id); + notify(); + } + }; +} + +export function getPluginComponent(id: string): AnyComponent | undefined { + return components.get(id); +} + +function subscribe(listener: () => void): () => void { + listeners.add(listener); + return () => listeners.delete(listener); +} + +/** The component registered under an id, re-rendering when that changes. */ +export function usePluginComponent(id: string): AnyComponent | undefined { + useSyncExternalStore(subscribe, () => version); + return components.get(id); +} + +/** + * Renders the component another plugin registered under `id`, with the rest + * of the props passed through, or `fallback` while nobody has registered it. + */ +export function PluginComponent({ + id, + fallback = null, + ...props +}: { + id: string; + fallback?: ReactNode; + [prop: string]: unknown; +}): ReactNode { + const Component = usePluginComponent(id); + if (!Component) return fallback as ReactNode; + return createElement(Component, props); +} + +/** Test seam. */ +export function resetPluginComponents(): void { + components.clear(); + notify(); +} diff --git a/src/ui/plugin-host/sdk-ui.ts b/src/ui/plugin-host/sdk-ui.ts index dedfbcc53..039951b4d 100644 --- a/src/ui/plugin-host/sdk-ui.ts +++ b/src/ui/plugin-host/sdk-ui.ts @@ -66,3 +66,61 @@ export { useTabs, useTabsSafe } from "@/shell/TabContext"; // Slots: a plugin can offer places for other plugins to fill. export { ActionSlot, ComponentSlot } from "@/shell/ActionSlot"; + +// Components other plugins offer by id (app.registerComponent), rendered with +// a fallback while their plugin is off. +export { PluginComponent } from "@/plugin-host/component-registry"; + +// More primitives and hooks. +export { cn } from "@/lib/utils"; +export { + runAdaptivePolling, + getPollingEnvironmentMultiplier, +} from "@/lib/adaptive-polling"; +export * from "@/components/sheet"; +export { useConfirmation } from "@/hooks/use-confirmation"; +export { useIsMobile } from "@/hooks/use-mobile"; +export { PassphraseDialog } from "@/ssh/dialogs/PassphraseDialog"; +export { OPKSSHDialog } from "@/ssh/dialogs/OPKSSHDialog"; +export { HostKeyVerificationDialog } from "@/ssh/dialogs/HostKeyVerificationDialog"; + +// Terminal look: themes, fonts and clipboard, shared by every terminal-like +// surface (the SSH and local terminals, the docker console, serial). +export * from "@/lib/terminal-themes"; +export { resolveTermixThemeColors } from "@/lib/terminal-look/terminal-theme"; +export { ensureTerminalFontsLoaded } from "@/lib/terminal-look/terminal-global-styles"; +export * from "@/lib/terminal-look/terminal-font-zoom"; +export { copyToClipboard, readFromClipboard } from "@/lib/clipboard"; +export { RobustClipboardProvider } from "@/lib/clipboard-provider"; +export { useTheme as useAppTheme } from "@/components/theme-provider"; + +// Keyboard handling the shell shares with a terminal, so app shortcuts keep +// working while a terminal has focus. +export { findMatchingKeybinding } from "@/lib/keybinding-match"; +export { globalShortcutHandler } from "@/lib/global-shortcut-handler"; +export { isMacPlatform, isTabJumpHotkey } from "@/lib/tab-jump-hotkey"; +export type * from "@/types/keybindings"; + +// Connection helpers: which backend a host's session dials, and the pieces +// the desktop app needs to reach it. +export { isElectron } from "@/lib/electron"; +export { + resolveConnectionOrigin, + type ConnectionOrigin, +} from "@/lib/connection-origin"; +export { pluginWsUrl } from "@/lib/plugin-transport"; +export { hydrateLocalSharedHostAuth } from "@/lib/remote-server-api"; +export { useConnectionDefaults } from "@/contexts/ConnectionDefaultsContext"; + +// Core APIs a terminal-like surface calls: recent activity, the host's stored +// password for sudo autofill, the open tab record, the user's keybindings and +// host terminal options. +export { getCookie } from "@/main-axios"; +export { logActivity } from "@/api/dashboard-api"; +export { getHostPassword } from "@/api/credentials-api"; +export { + patchOpenTab, + getUserPreferences, + parseCustomKeybindings, +} from "@/api/open-tabs-api"; +export { setHostAutoTmux } from "@/api/host-terminal-config-api"; diff --git a/src/ui/plugin-host/shell-bridge.ts b/src/ui/plugin-host/shell-bridge.ts index 8a3b74c13..365e1ce47 100644 --- a/src/ui/plugin-host/shell-bridge.ts +++ b/src/ui/plugin-host/shell-bridge.ts @@ -89,7 +89,6 @@ export const shell: TabShellCallbacks = { openSingletonTab: (...args) => callbacks?.openSingletonTab(...args), closeTab: (...args) => callbacks?.closeTab(...args), renameTab: (...args) => callbacks?.renameTab(...args), - openTerminalTab: (...args) => callbacks?.openTerminalTab(...args), openRailView: (...args) => callbacks?.openRailView(...args), closeRailView: (...args) => callbacks?.closeRailView(...args), saveQuickConnect: (...args) => diff --git a/src/ui/plugin-host/testing-host.tsx b/src/ui/plugin-host/testing-host.tsx index 86c0fbe7d..fdd0d5fe9 100644 --- a/src/ui/plugin-host/testing-host.tsx +++ b/src/ui/plugin-host/testing-host.tsx @@ -68,7 +68,6 @@ function recordingShell(calls: ShellCall[]): TabShellCallbacks { openSingletonTab: record("openSingletonTab"), closeTab: record("closeTab"), renameTab: record("renameTab"), - openTerminalTab: record("openTerminalTab"), openRailView: record("openRailView"), closeRailView: record("closeRailView"), saveQuickConnect: async (...args) => { diff --git a/src/ui/shell/CommandPalette.tsx b/src/ui/shell/CommandPalette.tsx index 42205e898..78d7981c6 100644 --- a/src/ui/shell/CommandPalette.tsx +++ b/src/ui/shell/CommandPalette.tsx @@ -13,7 +13,6 @@ import { import { Server, Settings, - Terminal, Globe, Plus, MessagesSquare, @@ -66,12 +65,11 @@ function getSshActions(host: Host): { }[] { return [ // --- tmux-monitor --- opt-in per host, off by default - host.enableTerminal !== false && - host.enableTmuxMonitor && { - type: "tmux_monitor", - icon: Layers, - label: "Tmux Monitor", - }, + host.enableTmuxMonitor && { + type: "tmux_monitor", + icon: Layers, + label: "Tmux Monitor", + }, ].filter(Boolean) as { type: TabType; icon: React.ElementType; @@ -173,11 +171,7 @@ export function CommandPalette({ setSelectedValue(`host-${firstHost.id}`); return; } - setSelectedValue( - window.electronAPI?.isElectron - ? "quick-action-local-terminal" - : "quick-action-add-host", - ); + setSelectedValue("quick-action-add-host"); }, [filteredHosts, isOpen, search]); const activeTargetTab = @@ -235,27 +229,6 @@ export function CommandPalette({ heading={t("commandPalette.quickActions")} className="px-2" > - {window.electronAPI?.isElectron && ( - <CommandItem - value="quick-action-local-terminal" - onSelect={() => - handleAction(() => onOpenTab("local-terminal")) - } - className="group flex items-center gap-3 px-3 py-2.5 rounded-none hover:bg-accent-brand/10 cursor-pointer" - > - <div className="size-8 rounded-none bg-muted flex items-center justify-center group-hover:bg-accent-brand/20 transition-colors"> - <Terminal className="size-4 text-accent-brand" /> - </div> - <div className="flex flex-col flex-1"> - <span className="text-sm font-semibold"> - {t("commandPalette.localTerminal")} - </span> - <span className="text-xs text-muted-foreground"> - {t("commandPalette.localTerminalDesc")} - </span> - </div> - </CommandItem> - )} <CommandItem value="quick-action-add-host" onSelect={() => diff --git a/src/ui/shell/TabBar.tsx b/src/ui/shell/TabBar.tsx index ebfc872eb..5cffc6845 100644 --- a/src/ui/shell/TabBar.tsx +++ b/src/ui/shell/TabBar.tsx @@ -36,13 +36,11 @@ import type { Tab, TabType, SplitMode } from "@/types/ui-types"; import { SPLIT_MODES, PANE_COUNTS } from "@/lib/theme"; /** - * Tabs holding a live connection that can be refreshed and shared: local - * terminals and registered session tabs. Serial sessions are not shareable. + * Tabs holding a live connection that can be refreshed and shared: the + * registered session tabs. Serial sessions are not shareable. */ function isConnectionTab(type: TabType): boolean { - return ( - type === "local-terminal" || (type !== "serial" && isSessionTabType(type)) - ); + return type !== "serial" && isSessionTabType(type); } export function TabBar({ @@ -689,7 +687,8 @@ export function TabBar({ {t("nav.refreshTab")} </button> )} - {ctxTab.type === "terminal" && + {typeof ctxTab.terminalRef?.current?.openFileManager === + "function" && ctxTab.host && onOpenFileManager && ( <button diff --git a/src/ui/shell/TabContext.tsx b/src/ui/shell/TabContext.tsx index e61737fc0..9a8e8027f 100644 --- a/src/ui/shell/TabContext.tsx +++ b/src/ui/shell/TabContext.tsx @@ -1,4 +1,5 @@ /* eslint-disable react-refresh/only-export-components */ +import { getTabType } from "./tab-registry"; import React, { createContext, useContext, @@ -175,8 +176,9 @@ export function TabProvider({ children }: TabProviderProps) { const id = nextTabId.current++; const instanceId = `tab_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + const isCommandTarget = !!getTabType(tabData.type)?.commandTarget; const needsUniqueTitle = - tabData.type === "terminal" || tabData.type === "file_manager"; + isCommandTarget || tabData.type === "file_manager"; const effectiveTitle = needsUniqueTitle ? computeUniqueTitle(tabData.type, tabData.title) : tabData.type === "tmux_monitor" // --- tmux-monitor --- @@ -187,10 +189,9 @@ export function TabProvider({ children }: TabProviderProps) { id, instanceId, title: effectiveTitle, - terminalRef: - tabData.type === "terminal" - ? React.createRef<TerminalRefHandle>() - : undefined, + terminalRef: isCommandTarget + ? React.createRef<TerminalRefHandle>() + : undefined, hostConfig: tabData.hostConfig ? { ...tabData.hostConfig, diff --git a/src/ui/shell/panel-registry.ts b/src/ui/shell/panel-registry.ts index 73630ef16..6131f3a81 100644 --- a/src/ui/shell/panel-registry.ts +++ b/src/ui/shell/panel-registry.ts @@ -3,6 +3,8 @@ import { createRegistry } from "@/lib/registry"; import type { TabShellCallbacks } from "./tab-registry"; export interface PanelRenderProps { + /** The command-target tab the user is working in, if any. */ + targetTab?: import("@/types/ui-types").Tab; active: boolean; shell: TabShellCallbacks; setEditing: (editing: boolean) => void; diff --git a/src/ui/shell/shell-layout.ts b/src/ui/shell/shell-layout.ts index a671cef3d..a25308370 100644 --- a/src/ui/shell/shell-layout.ts +++ b/src/ui/shell/shell-layout.ts @@ -25,7 +25,6 @@ const CORE_CAPTURABLE = new Set([ "termix-id", "session-logs", "macros", - "history", "ssh-tools", ]); @@ -36,14 +35,12 @@ const CORE_SINGLETON = new Set([ "termix-id", "session-logs", "macros", - "history", "ssh-tools", ]); /** Core tab types that are never part of a saved arrangement. */ const CORE_UNSAVED = new Set([ "dashboard", - "local-terminal", "host-manager", "user-profile", "admin-settings", diff --git a/src/ui/shell/tab-registry.ts b/src/ui/shell/tab-registry.ts index 296d11ba5..e93e4ba2d 100644 --- a/src/ui/shell/tab-registry.ts +++ b/src/ui/shell/tab-registry.ts @@ -23,7 +23,7 @@ export interface TabShellCallbacks { ) => void; closeTab: (tabId: string) => void; renameTab: (tabId: string, label: string) => void; - openTerminalTab: (host: Host, path?: string) => void; + openRailView: (id: string) => void; /** Closes a rail view wherever it is shown, e.g. when its feature goes away. */ closeRailView: (id: string) => void; @@ -69,6 +69,12 @@ export interface TabTypeDef { panelFrame?: boolean; /** False keeps this tab out of saved layouts and workspaces. */ inLayouts?: boolean; + /** History, macros and SSH tools act on the last one of these focused. */ + commandTarget?: boolean; + /** Paints its own background; its frame stays transparent. */ + ownBackground?: boolean; + /** Every open is a new tab. */ + multiInstance?: boolean; preload?: () => Promise<unknown>; } diff --git a/src/ui/shell/tabUtils.tsx b/src/ui/shell/tabUtils.tsx index 5fe69c905..ae74622cc 100644 --- a/src/ui/shell/tabUtils.tsx +++ b/src/ui/shell/tabUtils.tsx @@ -8,9 +8,7 @@ import { Settings, Usb, User, - TerminalSquare, Layers, // --- tmux-monitor --- - Clock, Fingerprint, Hammer, ScrollText, @@ -38,11 +36,7 @@ const CollabRoomTab = lazy(() => default: m.CollabRoomTab, })), ); -const LocalTerminal = lazy(() => - import("@/features/local-terminal/LocalTerminal").then((m) => ({ - default: m.LocalTerminal, - })), -); + const loadTmuxMonitor = () => import("@/features/tmux-monitor/TmuxMonitor").then((m) => ({ default: m.TmuxMonitor, @@ -75,9 +69,7 @@ const SessionLogsPanel = lazy(() => const MacrosPanel = lazy(() => import("@/sidebar/MacrosPanel").then((m) => ({ default: m.MacrosPanel })), ); -const HistoryPanel = lazy(() => - import("@/sidebar/HistoryPanel").then((m) => ({ default: m.HistoryPanel })), -); + const SshToolsPanel = lazy(() => import("@/sidebar/SshToolsPanel").then((m) => ({ default: m.SshToolsPanel })), ); @@ -146,8 +138,6 @@ export function tabIcon(type: TabType) { switch (type) { case "dashboard": return <LayoutDashboard className="size-3.5" />; - case "local-terminal": - return <TerminalSquare className="size-3.5" />; case "host-manager": return <Server className="size-3.5" />; case "user-profile": @@ -169,8 +159,7 @@ export function tabIcon(type: TabType) { return <ScrollText className="size-3.5" />; case "macros": return <Braces className="size-3.5" />; - case "history": - return <Clock className="size-3.5" />; + case "ssh-tools": return <Hammer className="size-3.5" />; case "split-screen": @@ -262,11 +251,6 @@ export function renderTabContent(tab: Tab, context: TabRenderContext) { />, ); - case "local-terminal": - return withTabSuspense( - <LocalTerminal instanceId={tab.instanceId} isVisible={isVisible} />, - ); - // --- tmux-monitor --- case "tmux_monitor": return withTabSuspense( @@ -324,16 +308,6 @@ export function renderTabContent(tab: Tab, context: TabRenderContext) { </PanelTabFrame>, ); - case "history": - return withTabSuspense( - <PanelTabFrame> - <HistoryPanel - terminalTabs={panelProps?.terminalTabs ?? []} - activeTabId={panelProps?.targetTerminalTabId ?? ""} - /> - </PanelTabFrame>, - ); - case "ssh-tools": return withTabSuspense( <PanelTabFrame> diff --git a/src/ui/sidebar/AdminImageStorageSection.tsx b/src/ui/sidebar/AdminImageStorageSection.tsx deleted file mode 100644 index 92473d507..000000000 --- a/src/ui/sidebar/AdminImageStorageSection.tsx +++ /dev/null @@ -1,249 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { ImageIcon } from "lucide-react"; -import { Button } from "@/components/button"; -import { Input } from "@/components/input"; -import type { - TerminalImageStorageMode, - TerminalImageStorageSettings, - TerminalImageStorageTestResult, -} from "@/api/settings-api"; -import { AccordionSection } from "./AdminSettingsShared"; - -const MODE_LABEL_KEYS: Record<TerminalImageStorageMode, string> = { - auto: "admin.imageStorageModeAuto", - local: "admin.imageStorageModeLocal", - "remote-sftp": "admin.imageStorageModeRemoteSftp", -}; - -const SELECTED_MODE_LABEL_KEYS: Record< - TerminalImageStorageTestResult["selectedMode"], - string -> = { - local: "admin.imageStorageModeLocal", - "remote-sftp": "admin.imageStorageModeRemoteSftp", - unavailable: "admin.imageStorageSelectedModeUnavailable", -}; - -export function AdminImageStorageSection({ - open, - onToggle, - settings, - setSettings, - localDir, - setLocalDir, - instanceId, - setInstanceId, - saving, - testing, - testResult, - onSave, - onTest, -}: { - open: boolean; - onToggle: () => void; - settings: TerminalImageStorageSettings | null; - setSettings: (settings: TerminalImageStorageSettings) => void; - localDir: string; - setLocalDir: (value: string) => void; - instanceId: string; - setInstanceId: (value: string) => void; - saving: boolean; - testing: boolean; - testResult: TerminalImageStorageTestResult | null; - onSave: () => void; - onTest: () => void; -}) { - const { t } = useTranslation(); - - return ( - <AccordionSection - label={t("admin.sectionImageStorage")} - icon={<ImageIcon className="size-3.5" />} - open={open} - onToggle={onToggle} - > - {settings === null ? ( - <div className="pt-3 text-xs text-muted-foreground"> - {t("common.loading")} - </div> - ) : ( - <div className="@container flex flex-col gap-0 pt-2"> - <div className="flex flex-col gap-2 py-3 border-b border-border"> - <div className="flex flex-col gap-0.5"> - <span className="text-sm font-medium leading-snug"> - {t("admin.imageStorageMode")} - </span> - <span className="text-xs text-muted-foreground leading-snug"> - {t("admin.imageStorageModeDesc")} - </span> - </div> - <div className="flex flex-wrap gap-1"> - {(Object.keys(MODE_LABEL_KEYS) as TerminalImageStorageMode[]).map( - (mode) => ( - <button - key={mode} - type="button" - aria-pressed={settings.mode === mode} - onClick={() => setSettings({ ...settings, mode })} - className={`whitespace-nowrap px-2 py-1 text-[10px] font-semibold border transition-colors ${settings.mode === mode ? "border-accent-brand/40 bg-accent-brand/10 text-accent-brand" : "border-border text-muted-foreground hover:text-foreground"}`} - > - {t(MODE_LABEL_KEYS[mode])} - </button> - ), - )} - </div> - </div> - - <div className="grid grid-cols-1 gap-x-4 gap-y-3 py-3 @md:grid-cols-2 border-b border-border"> - <label className="flex flex-col gap-1 text-xs"> - <span className="font-medium text-foreground"> - {t("admin.imageStorageLocalDir")} - </span> - <Input - aria-label={t("admin.imageStorageLocalDir")} - value={localDir} - placeholder={t("admin.imageStorageLocalDirPlaceholder")} - onChange={(event) => setLocalDir(event.target.value)} - className="h-8 text-xs" - /> - <span className="text-muted-foreground"> - {t("admin.imageStorageLocalDirDesc")} - </span> - </label> - <label className="flex flex-col gap-1 text-xs"> - <span className="font-medium text-foreground"> - {t("admin.imageStorageHostPath")} - </span> - <Input - aria-label={t("admin.imageStorageHostPath")} - value={settings.hostPath} - onChange={(event) => - setSettings({ ...settings, hostPath: event.target.value }) - } - className="h-8 text-xs" - /> - <span className="text-muted-foreground"> - {t("admin.imageStorageHostPathDesc")} - </span> - </label> - <label className="flex flex-col gap-1 text-xs"> - <span className="font-medium text-foreground"> - {t("admin.imageStorageTtl")} - </span> - <Input - aria-label={t("admin.imageStorageTtl")} - type="number" - min={0} - value={settings.ttlMs} - onChange={(event) => - setSettings({ - ...settings, - ttlMs: Number(event.target.value), - }) - } - className="h-8 text-xs" - /> - </label> - <label className="flex flex-col gap-1 text-xs"> - <span className="font-medium text-foreground"> - {t("admin.imageStorageMaxCount")} - </span> - <Input - aria-label={t("admin.imageStorageMaxCount")} - type="number" - min={1} - value={settings.maxCount} - onChange={(event) => - setSettings({ - ...settings, - maxCount: Number(event.target.value), - }) - } - className="h-8 text-xs" - /> - </label> - <label className="flex flex-col gap-1 text-xs"> - <span className="font-medium text-foreground"> - {t("admin.imageStorageMaxBytes")} - </span> - <Input - aria-label={t("admin.imageStorageMaxBytes")} - type="number" - min={1048576} - value={settings.maxBytes} - onChange={(event) => - setSettings({ - ...settings, - maxBytes: Number(event.target.value), - }) - } - className="h-8 text-xs" - /> - </label> - </div> - - <div className="flex flex-col gap-2 py-3 border-b border-border"> - <div className="flex flex-col gap-0.5"> - <span className="text-sm font-medium leading-snug"> - {t("admin.imageStorageInstanceId")} - </span> - <span className="text-xs text-muted-foreground leading-snug"> - {t("admin.imageStorageInstanceIdDesc")} - </span> - </div> - <Input - aria-label={t("admin.imageStorageInstanceId")} - value={instanceId} - onChange={(event) => setInstanceId(event.target.value)} - className="h-8 w-full @md:w-64 text-xs" - /> - </div> - - {testResult && ( - <div className="py-2 text-xs text-muted-foreground border-b border-border"> - <div> - {testResult.connected - ? t("admin.imageStorageTestConnected") - : t("admin.imageStorageTestNotConnected")} - </div> - <div> - {t("admin.imageStorageTestSelectedMode", { - mode: t(SELECTED_MODE_LABEL_KEYS[testResult.selectedMode]), - })} - </div> - {testResult.localHostVisible !== null && ( - <div> - {testResult.localHostVisible - ? t("admin.imageStorageTestLocalVisible") - : t("admin.imageStorageTestLocalNotVisible")} - </div> - )} - </div> - )} - - <div className="flex gap-2 pt-3"> - <Button - size="sm" - variant="outline" - className="border-accent-brand/40 text-accent-brand hover:bg-accent-brand/10 hover:text-accent-brand" - disabled={saving} - onClick={onSave} - > - {saving ? t("common.saving") : t("common.save")} - </Button> - <Button - size="sm" - variant="outline" - disabled={testing} - onClick={onTest} - > - {testing - ? t("admin.imageStorageTesting") - : t("admin.imageStorageTest")} - </Button> - </div> - </div> - )} - </AccordionSection> - ); -} diff --git a/src/ui/sidebar/AdminSettingsPanel.tsx b/src/ui/sidebar/AdminSettingsPanel.tsx index b425f0dc8..ce47347ff 100644 --- a/src/ui/sidebar/AdminSettingsPanel.tsx +++ b/src/ui/sidebar/AdminSettingsPanel.tsx @@ -41,8 +41,6 @@ import { updateSecondFactorAfterExternalLogin, getOidcSilentLoginDefault, updateOidcSilentLoginDefault, - getCommandHistoryEnabled, - updateCommandHistoryEnabled, isElectron, getUserRoles, } from "@/main-axios"; @@ -51,14 +49,9 @@ import { updateHostDefaults, getAnalyticsEnabled, updateAnalyticsEnabled, - getTerminalImageStorageSettings, - updateTerminalImageStorageSettings, - testTerminalImageStorage, getBranding, updateBranding, type HostDefaults, - type TerminalImageStorageSettings, - type TerminalImageStorageTestResult, type BrandingSettings, } from "@/api/settings-api"; import { @@ -94,12 +87,7 @@ import { type AdminUser, } from "./AdminManagementSections"; import { toast } from "sonner"; -import { - getTerminalSessionSettings, - updateTerminalSessionSettings, - getStepCaSettings, - updateStepCaSettings, -} from "@/api/settings-api"; +import { getStepCaSettings, updateStepCaSettings } from "@/api/settings-api"; import { getDatabaseTransferUrl } from "@/lib/database-transfer-url"; import { AdminDatabaseSection, @@ -118,17 +106,6 @@ import { AdminUnlinkAccountDialog, } from "./AdminUserDialogs"; import { AdminUserManagePanel } from "./AdminUserManagePanel"; -import { - TOUCH_INPUT_DEFAULTS, - type TouchInputSettings, -} from "@/types/touch-input-settings"; -import { - getTouchInputSettings, - updateTouchInputSettings, -} from "@/api/touch-input-settings-api"; -import { cacheTouchInputSettings } from "@/features/terminal/touch-input-settings-store"; -import { AdminTouchInputSection } from "./AdminTouchInputSection"; -import { AdminImageStorageSection } from "./AdminImageStorageSection"; import { AdminBrandingSection } from "./AdminBrandingSection"; type ApiErrorLike = { @@ -162,18 +139,11 @@ export function AdminSettingsPanel({ const [passwordLoginForced, setPasswordLoginForced] = useState(false); const [allowPasswordReset, setAllowPasswordReset] = useState(true); const [sessionTimeout, setSessionTimeout] = useState("24"); - const [terminalTimeout, setTerminalTimeout] = useState("30"); - useEffect(() => { - getTerminalSessionSettings() - .then((settings) => setTerminalTimeout(String(settings.timeoutMinutes))) - .catch(() => {}); - }, []); const [statusInterval, setStatusInterval] = useState("60"); const [metricsInterval, setMetricsInterval] = useState("30"); const [guacEnabled, setGuacEnabled] = useState(false); const [guacUrl, setGuacUrl] = useState("guacd:4822"); const [logLevel, setLogLevel] = useState("info"); - const [commandHistoryEnabled, setCommandHistoryEnabled] = useState(true); const [analyticsEnabled, setAnalyticsEnabled] = useState(true); const [analyticsLocked, setAnalyticsLocked] = useState(false); const [oidcSilentLoginDefaultLocked, setOidcSilentLoginDefaultLocked] = @@ -204,19 +174,9 @@ export function AdminSettingsPanel({ const [notificationPrivateEndpoints, setNotificationPrivateEndpoints] = useState<string[]>([]); const [hostDefaults, setHostDefaults] = useState<HostDefaults>({}); - const [touchInputSettings, setTouchInputSettings] = - useState<TouchInputSettings>({ ...TOUCH_INPUT_DEFAULTS }); // Terminal image storage state. localDir stays a draft: the API never // returns the configured backend path, so it is only sent when changed. - const [imageStorageSettings, setImageStorageSettings] = - useState<TerminalImageStorageSettings | null>(null); - const [imageStorageLocalDir, setImageStorageLocalDir] = useState(""); - const [imageStorageInstanceId, setImageStorageInstanceId] = useState(""); - const [imageStorageSaving, setImageStorageSaving] = useState(false); - const [imageStorageTesting, setImageStorageTesting] = useState(false); - const [imageStorageTestResult, setImageStorageTestResult] = - useState<TerminalImageStorageTestResult | null>(null); const [brandingSettings, setBrandingSettings] = useState<BrandingSettings | null>(null); @@ -400,14 +360,11 @@ export function AdminSettingsPanel({ oidcProv, secondFactorExternal, oidcSilent, - cmdHistory, analytics, sessionSharingEnabled, - touchInput, notificationEndpoints, stepCaEndpoints, secretSourceEndpoints, - imageStorage, branding, ] = await Promise.allSettled([ getRegistrationAllowed(), @@ -420,14 +377,11 @@ export function AdminSettingsPanel({ getOidcAutoProvision(), getSecondFactorAfterExternalLogin(), getOidcSilentLoginDefault(), - getCommandHistoryEnabled(), getAnalyticsEnabled(), getSessionSharingGloballyEnabled(), - getTouchInputSettings(), getNotificationPrivateEndpoints(), getStepCaPrivateEndpoints(), getSecretSourcePrivateEndpoints(), - getTerminalImageStorageSettings(), getBranding(), ]); @@ -460,9 +414,6 @@ export function AdminSettingsPanel({ setGuacEnabled(guac.value.enabled); setGuacUrl(guac.value.url || "guacd:4822"); } - if (cmdHistory.status === "fulfilled") { - setCommandHistoryEnabled(cmdHistory.value.enabled); - } if (analytics.status === "fulfilled") { setAnalyticsEnabled(analytics.value.enabled); setAnalyticsLocked(analytics.value.locked ?? false); @@ -470,10 +421,6 @@ export function AdminSettingsPanel({ if (sessionSharingEnabled.status === "fulfilled") { setSessionSharingGloballyEnabled(sessionSharingEnabled.value.enabled); } - if (touchInput.status === "fulfilled") { - setTouchInputSettings(touchInput.value); - cacheTouchInputSettings(touchInput.value); - } if (stepCaEndpoints.status === "fulfilled") { setStepCaPrivateEndpoints(stepCaEndpoints.value); } @@ -483,9 +430,6 @@ export function AdminSettingsPanel({ if (notificationEndpoints.status === "fulfilled") { setNotificationPrivateEndpoints(notificationEndpoints.value); } - if (imageStorage.status === "fulfilled") { - setImageStorageSettings(imageStorage.value); - } if (branding.status === "fulfilled") { setBrandingSettings(branding.value); } @@ -597,17 +541,6 @@ export function AdminSettingsPanel({ } } - async function handleToggleCommandHistory() { - const newVal = !commandHistoryEnabled; - setCommandHistoryEnabled(newVal); - try { - await updateCommandHistoryEnabled(newVal); - } catch { - setCommandHistoryEnabled(!newVal); - toast.error(t("admin.updateCommandHistoryFailed")); - } - } - async function handleToggleAnalytics() { if (analyticsLocked) return; const newVal = !analyticsEnabled; @@ -679,47 +612,6 @@ export function AdminSettingsPanel({ } } - async function saveTouchInputSettings(settings = touchInputSettings) { - try { - const saved = await updateTouchInputSettings(settings); - setTouchInputSettings(saved); - cacheTouchInputSettings(saved); - toast.success(t("admin.touchSaved")); - } catch { - toast.error(t("admin.touchSaveFailed")); - } - } - - function resetTouchInputSettings() { - const defaults = { ...TOUCH_INPUT_DEFAULTS }; - setTouchInputSettings(defaults); - void saveTouchInputSettings(defaults); - } - - async function handleSaveImageStorage() { - if (!imageStorageSettings) return; - setImageStorageSaving(true); - try { - const saved = await updateTerminalImageStorageSettings({ - mode: imageStorageSettings.mode, - hostPath: imageStorageSettings.hostPath, - ttlMs: imageStorageSettings.ttlMs, - maxCount: imageStorageSettings.maxCount, - maxBytes: imageStorageSettings.maxBytes, - ...(imageStorageLocalDir.trim() - ? { localDir: imageStorageLocalDir.trim() } - : {}), - }); - setImageStorageSettings(saved); - setImageStorageLocalDir(""); - toast.success(t("admin.imageStorageSaved")); - } catch (e) { - toast.error(apiErrorMessage(e, t("admin.imageStorageSaveFailed"))); - } finally { - setImageStorageSaving(false); - } - } - async function handleSaveBranding() { if (!brandingSettings) return; setBrandingSaving(true); @@ -754,24 +646,6 @@ export function AdminSettingsPanel({ } } - async function handleTestImageStorage() { - if (!imageStorageInstanceId.trim()) { - toast.error(t("admin.imageStorageInstanceIdRequired")); - return; - } - setImageStorageTesting(true); - setImageStorageTestResult(null); - try { - setImageStorageTestResult( - await testTerminalImageStorage(imageStorageInstanceId.trim()), - ); - } catch (e) { - toast.error(apiErrorMessage(e, t("admin.imageStorageTestFailed"))); - } finally { - setImageStorageTesting(false); - } - } - async function handleSaveSessionTimeout() { const hours = parseInt(sessionTimeout, 10); if (isNaN(hours) || hours < 1 || hours > 720) { @@ -786,20 +660,6 @@ export function AdminSettingsPanel({ } } - async function handleSaveTerminalTimeout() { - const minutes = parseInt(terminalTimeout, 10); - if (isNaN(minutes) || minutes < 1 || minutes > 1440) { - toast.error(t("admin.terminalSessionTimeoutRange")); - return; - } - try { - await updateTerminalSessionSettings({ timeoutMinutes: minutes }); - toast.success(t("admin.terminalSessionTimeoutSaved")); - } catch { - toast.error(t("admin.terminalSessionTimeoutSaveFailed")); - } - } - async function handleSaveMonitoring() { const status = parseInt(statusInterval, 10); const metrics = parseInt(metricsInterval, 10); @@ -1261,14 +1121,9 @@ export function AdminSettingsPanel({ handleToggleOidcSilentLoginDefault={handleToggleOidcSilentLoginDefault} allowPasswordReset={allowPasswordReset} handleTogglePasswordReset={handleTogglePasswordReset} - commandHistoryEnabled={commandHistoryEnabled} - handleToggleCommandHistory={handleToggleCommandHistory} sessionTimeout={sessionTimeout} setSessionTimeout={setSessionTimeout} handleSaveSessionTimeout={handleSaveSessionTimeout} - terminalTimeout={terminalTimeout} - setTerminalTimeout={setTerminalTimeout} - handleSaveTerminalTimeout={handleSaveTerminalTimeout} statusInterval={statusInterval} setStatusInterval={setStatusInterval} metricsInterval={metricsInterval} @@ -1355,22 +1210,6 @@ export function AdminSettingsPanel({ handleSaveDefaults={handleSaveHostDefaults} /> - <AdminImageStorageSection - open={openSections.has("image-storage")} - onToggle={() => toggle("image-storage")} - settings={imageStorageSettings} - setSettings={setImageStorageSettings} - localDir={imageStorageLocalDir} - setLocalDir={setImageStorageLocalDir} - instanceId={imageStorageInstanceId} - setInstanceId={setImageStorageInstanceId} - saving={imageStorageSaving} - testing={imageStorageTesting} - testResult={imageStorageTestResult} - onSave={() => void handleSaveImageStorage()} - onTest={() => void handleTestImageStorage()} - /> - <AdminBrandingSection open={openSections.has("branding")} onToggle={() => toggle("branding")} @@ -1442,15 +1281,6 @@ export function AdminSettingsPanel({ onToggle={() => toggle("plugins")} /> - <AdminTouchInputSection - open={openSections.has("touch-input")} - onToggle={() => toggle("touch-input")} - settings={touchInputSettings} - setSettings={setTouchInputSettings} - onSave={() => void saveTouchInputSettings()} - onReset={resetTouchInputSettings} - /> - <AdminCreateUserDialog open={createUserOpen} onOpenChange={setCreateUserOpen} diff --git a/src/ui/sidebar/AdminSettingsSections.tsx b/src/ui/sidebar/AdminSettingsSections.tsx index 05191cc9d..8ab4e2b36 100644 --- a/src/ui/sidebar/AdminSettingsSections.tsx +++ b/src/ui/sidebar/AdminSettingsSections.tsx @@ -54,14 +54,9 @@ type GeneralSettingsSectionProps = { handleToggleOidcSilentLoginDefault: () => void; allowPasswordReset: boolean; handleTogglePasswordReset: () => void; - commandHistoryEnabled: boolean; - handleToggleCommandHistory: () => void; sessionTimeout: string; setSessionTimeout: Dispatch<SetStateAction<string>>; handleSaveSessionTimeout: () => void; - terminalTimeout: string; - setTerminalTimeout: Dispatch<SetStateAction<string>>; - handleSaveTerminalTimeout: () => void; statusInterval: string; setStatusInterval: Dispatch<SetStateAction<string>>; metricsInterval: string; @@ -107,14 +102,9 @@ export function AdminGeneralSettingsSection({ handleToggleOidcSilentLoginDefault, allowPasswordReset, handleTogglePasswordReset, - commandHistoryEnabled, - handleToggleCommandHistory, sessionTimeout, setSessionTimeout, handleSaveSessionTimeout, - terminalTimeout, - setTerminalTimeout, - handleSaveTerminalTimeout, statusInterval, setStatusInterval, metricsInterval, @@ -337,16 +327,6 @@ export function AdminGeneralSettingsSection({ onToggle={handleTogglePasswordReset} /> </SettingRow> - <SettingRow - label={t("admin.commandHistoryEnabled")} - description={t("admin.commandHistoryEnabledDesc")} - > - <AdminToggle - on={commandHistoryEnabled} - onToggle={handleToggleCommandHistory} - /> - </SettingRow> - <div className="flex flex-col gap-2 pt-3 mt-2"> <span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground"> {t("admin.sessionTimeout")} @@ -377,36 +357,6 @@ export function AdminGeneralSettingsSection({ </span> </div> - <div className="flex flex-col gap-2 pt-3 mt-2"> - <span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground"> - {t("admin.terminalSessionTimeout")} - </span> - <div className="flex items-center gap-2"> - <Input - type="number" - min={1} - max={1440} - value={terminalTimeout} - onChange={(e) => setTerminalTimeout(e.target.value)} - className="w-20 text-sm" - /> - <span className="text-xs text-muted-foreground"> - {t("admin.minutes")} - </span> - <Button - variant="outline" - size="sm" - className="text-xs border-accent-brand/40 text-accent-brand hover:bg-accent-brand/10 hover:text-accent-brand h-7" - onClick={handleSaveTerminalTimeout} - > - {t("common.save")} - </Button> - </div> - <span className="text-[10px] text-muted-foreground"> - {t("admin.terminalSessionTimeoutDesc")} - </span> - </div> - <div className="flex flex-col gap-2 border-t border-border pt-3 mt-2"> <span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground"> {t("admin.monitoringDefaults")} diff --git a/src/ui/sidebar/AdminTouchInputSection.tsx b/src/ui/sidebar/AdminTouchInputSection.tsx deleted file mode 100644 index 986056b50..000000000 --- a/src/ui/sidebar/AdminTouchInputSection.tsx +++ /dev/null @@ -1,170 +0,0 @@ -import { useState } from "react"; -import { useTranslation } from "react-i18next"; -import { ChevronDown, Hand } from "lucide-react"; -import { Button } from "@/components/button"; -import { Input } from "@/components/input"; -import { SettingRow } from "@/components/section-card"; -import type { - TouchInputNumericKey, - TouchInputSettings, -} from "@/types/touch-input-settings"; -import { TOUCH_INPUT_NUMERIC_BOUNDS } from "@/types/touch-input-settings"; -import { AccordionSection, AdminToggle } from "./AdminSettingsShared"; - -const fields: Array<{ - key: TouchInputNumericKey; - label: string; - unit: string; -}> = [ - { key: "dragThresholdPx", label: "touchDragThreshold", unit: "px" }, - { key: "maxWheelDeltaPx", label: "touchMaxWheelDelta", unit: "px" }, - { - key: "momentumSampleWindowMs", - label: "touchMomentumSampleWindow", - unit: "ms", - }, - { - key: "releaseGracePeriodMs", - label: "touchReleaseGracePeriod", - unit: "ms", - }, - { - key: "minimumVelocityPxPerMs", - label: "touchMinimumVelocity", - unit: "px/ms", - }, - { - key: "maximumVelocityPxPerMs", - label: "touchMaximumVelocity", - unit: "px/ms", - }, - { key: "maximumDurationMs", label: "touchMaximumDuration", unit: "ms" }, - { key: "maximumTravelPx", label: "touchMaximumTravel", unit: "px" }, - { key: "decayTimeMs", label: "touchDecayTime", unit: "ms" }, - { key: "pixelsPerTick", label: "touchPixelsPerTick", unit: "px" }, - { - key: "maximumFrameIntervalMs", - label: "touchMaximumFrameInterval", - unit: "ms", - }, - { - key: "maximumTicksPerFrame", - label: "touchMaximumTicksPerFrame", - unit: "", - }, -]; - -export function AdminTouchInputSection({ - open, - onToggle, - settings, - setSettings, - onSave, - onReset, -}: { - open: boolean; - onToggle: () => void; - settings: TouchInputSettings; - setSettings: (settings: TouchInputSettings) => void; - onSave: () => void; - onReset: () => void; -}) { - const { t } = useTranslation(); - const [advancedOpen, setAdvancedOpen] = useState(false); - - return ( - <AccordionSection - label={t("admin.sectionTouchInput")} - icon={<Hand className="size-3.5" />} - open={open} - onToggle={onToggle} - > - <div className="flex flex-col gap-0 pt-2"> - <SettingRow - label={t("admin.touchEnabled")} - description={t("admin.touchEnabledDesc")} - > - <AdminToggle - on={settings.enabled} - onToggle={() => - setSettings({ ...settings, enabled: !settings.enabled }) - } - /> - </SettingRow> - <SettingRow - label={t("admin.touchMomentumEnabled")} - description={t("admin.touchMomentumEnabledDesc")} - > - <AdminToggle - on={settings.momentumEnabled} - onToggle={() => - setSettings({ - ...settings, - momentumEnabled: !settings.momentumEnabled, - }) - } - disabled={!settings.enabled} - /> - </SettingRow> - - <button - type="button" - className="flex items-center gap-2 py-3 text-left text-xs font-semibold text-foreground" - onClick={() => setAdvancedOpen((value) => !value)} - > - <ChevronDown - className={`size-3.5 transition-transform ${advancedOpen ? "rotate-180" : ""}`} - /> - {t("admin.touchAdvanced")} - </button> - {advancedOpen && ( - <div className="grid grid-cols-1 gap-3 pb-3 sm:grid-cols-2"> - {fields.map(({ key, label, unit }) => { - const bounds = TOUCH_INPUT_NUMERIC_BOUNDS[key]; - return ( - <label key={key} className="flex flex-col gap-1 text-xs"> - <span className="font-medium text-foreground"> - {t(`admin.${label}`)} - </span> - <div className="flex items-center gap-2"> - <Input - aria-label={t(`admin.${label}`)} - type="number" - min={bounds.min} - max={bounds.max} - step={bounds.step} - value={settings[key]} - onChange={(event) => - setSettings({ - ...settings, - [key]: Number(event.target.value), - }) - } - className="h-8 text-xs" - /> - {unit && ( - <span className="w-10 text-muted-foreground">{unit}</span> - )} - </div> - </label> - ); - })} - </div> - )} - <div className="flex gap-2 border-t border-border pt-3"> - <Button - size="sm" - variant="outline" - className="border-accent-brand/40 text-accent-brand hover:bg-accent-brand/10 hover:text-accent-brand" - onClick={onSave} - > - {t("common.save")} - </Button> - <Button size="sm" variant="outline" onClick={onReset}> - {t("admin.touchResetDefaults")} - </Button> - </div> - </div> - </AccordionSection> - ); -} diff --git a/src/ui/sidebar/ConnectionDefaultsSettings.tsx b/src/ui/sidebar/ConnectionDefaultsSettings.tsx index fc7fdd712..8093dc1ff 100644 --- a/src/ui/sidebar/ConnectionDefaultsSettings.tsx +++ b/src/ui/sidebar/ConnectionDefaultsSettings.tsx @@ -105,7 +105,7 @@ export function ConnectionDefaultsSettings() { const { t } = useTranslation(); const defaults = useConnectionDefaults(); const [open, setOpen] = useState(false); - const [tab, setTab] = useState<"terminal" | "rdp">("terminal"); + const [tab, setTab] = useState<"shell" | "desktop">("shell"); const [terminal, setTerminal] = useState<TerminalDefaults>({}); const [rdp, setRdp] = useState<RemoteDesktopDefaults>({}); const [saving, setSaving] = useState(false); @@ -154,8 +154,8 @@ export function ConnectionDefaultsSettings() { setSaving(true); try { await Promise.all([ - defaults.saveDefaults("terminal", terminal), - defaults.saveDefaults("rdp", rdp), + defaults.saveTerminalDefaults(terminal), + defaults.saveRdpDefaults(rdp), ]); toast.success(t("newUi.sidebar.connectionDefaults.saved")); setOpen(false); @@ -207,8 +207,11 @@ export function ConnectionDefaultsSettings() { <div className="flex border-b border-border"> {( [ - ["terminal", "newUi.sidebar.connectionDefaults.tabTerminal"], - ["rdp", "newUi.sidebar.connectionDefaults.tabRemoteDesktop"], + ["shell", "newUi.sidebar.connectionDefaults.tabTerminal"], + [ + "desktop", + "newUi.sidebar.connectionDefaults.tabRemoteDesktop", + ], ] as const ).map(([id, labelKey]) => ( <button @@ -226,7 +229,7 @@ export function ConnectionDefaultsSettings() { </div> <div className="max-h-[55vh] overflow-y-auto pr-1"> - {tab === "terminal" ? ( + {tab === "shell" ? ( <div className="grid grid-cols-1 sm:grid-cols-2 gap-3"> <Field label={t("hosts.fontFamilyLabel")}> <select diff --git a/src/ui/sidebar/ConnectionsPanel.tsx b/src/ui/sidebar/ConnectionsPanel.tsx index a79be6663..2c7e2bd79 100644 --- a/src/ui/sidebar/ConnectionsPanel.tsx +++ b/src/ui/sidebar/ConnectionsPanel.tsx @@ -1,7 +1,15 @@ import { getTabType, isPersistentTabType } from "@/shell/tab-registry"; import { useState, useEffect, useRef, useCallback } from "react"; import { useTranslation } from "react-i18next"; -import { ExternalLink, Plug, Search, X, Pencil, Check } from "lucide-react"; +import { + ExternalLink, + Plug, + Search, + X, + Pencil, + Check, + SquareTerminal, +} from "lucide-react"; import { getActiveSessions, deleteOpenTab, @@ -9,6 +17,7 @@ import { type OpenTabRecord, } from "@/main-axios"; import { tabIcon } from "@/shell/tabUtils"; +import { getSessionTimeoutMinutes } from "@/api/open-tabs-api"; import type { Tab, TabType } from "@/types/ui-types"; import { Badge } from "@/components/badge"; import { Button } from "@/components/button"; @@ -309,9 +318,8 @@ export function ConnectionsPanel({ const [persistMinutes, setPersistMinutes] = useState(30); useEffect(() => { - import("@/api/settings-api") - .then(({ getTerminalSessionSettings }) => getTerminalSessionSettings()) - .then((settings) => setPersistMinutes(settings.timeoutMinutes)) + getSessionTimeoutMinutes() + .then((minutes) => setPersistMinutes(minutes)) .catch(() => {}); }, []); const [activeSessions, setActiveSessions] = useState<ActiveSessionInfo[]>([]); @@ -510,10 +518,10 @@ export function ConnectionsPanel({ const liveSession = tab.instanceId ? sessionByInstanceId.get(tab.instanceId) : undefined; - const isLive = - tab.type === "terminal" - ? (liveSession?.isConnected ?? false) - : true; + const tracksSession = !!getTabType(tab.type)?.commandTarget; + const isLive = tracksSession + ? (liveSession?.isConnected ?? false) + : true; const duration = liveSession?.createdAt ? formatDuration(now - liveSession.createdAt) : formatDuration(now - tab.openedAt); @@ -552,7 +560,7 @@ export function ConnectionsPanel({ name={displayName} hostName={tab.customLabel ? hostName : undefined} subLabel={ - isLive && tab.type === "terminal" + isLive && tracksSession ? t("connections.connectedFor", { duration }) : isLive ? t("connections.connected") @@ -655,7 +663,7 @@ function SharedWithMeRow({ return ( <div className="group flex items-center gap-2.5 px-3 py-2.5 border-b border-border/40 last:border-b-0"> <div className="shrink-0 flex items-center justify-center size-7 rounded bg-muted/60 text-muted-foreground"> - {tabIcon("terminal")} + <SquareTerminal className="size-3.5" /> </div> <div className="flex flex-col flex-1 min-w-0 gap-0.5"> <div className="flex items-center gap-1.5 min-w-0"> diff --git a/src/ui/sidebar/HostEditor.tsx b/src/ui/sidebar/HostEditor.tsx index a3747daae..45e130177 100644 --- a/src/ui/sidebar/HostEditor.tsx +++ b/src/ui/sidebar/HostEditor.tsx @@ -21,7 +21,7 @@ import { import { TERMINAL_FONT_ZOOM_MIN, TERMINAL_FONT_ZOOM_MAX, -} from "@/features/terminal/terminal-font-zoom"; +} from "@/lib/terminal-look/terminal-font-zoom"; import { Globe, Info, @@ -36,7 +36,7 @@ import { } from "lucide-react"; import { toast } from "sonner"; import { SectionCard, SettingRow, FakeSwitch } from "@/components/section-card"; -import { TerminalPreview } from "@/features/terminal/TerminalPreview"; +import { TerminalPreview } from "@/components/terminal-preview/TerminalPreview"; import { createSSHHost, updateSSHHost, @@ -1169,7 +1169,7 @@ export function HostEditor({ </> )} - {activeTab === "terminal" && ( + {activeTab === "terminal-options" && ( <> <SectionCard title={t("hosts.terminalAppearance")} @@ -1820,27 +1820,6 @@ export function HostEditor({ onChange={(v) => setField("allowSessionSharing", v)} /> </SettingRow> - <SettingRow - label={t("hosts.enableCommandHistory")} - description={ - <> - {t("hosts.enableCommandHistoryDesc")}{" "} - <a - href="https://docs.termix.site/features/terminal/command-history" - target="_blank" - rel="noreferrer" - className="text-accent-brand hover:underline" - > - {t("hosts.docsLink")} - </a> - </> - } - > - <FakeSwitch - checked={form.enableCommandHistory} - onChange={(v) => setField("enableCommandHistory", v)} - /> - </SettingRow> <div className="flex flex-col gap-1.5"> <label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground"> {t("hosts.localEchoLabel")} diff --git a/src/ui/sidebar/HostManagerTabs.tsx b/src/ui/sidebar/HostManagerTabs.tsx index 4d7db36b1..f3d31f630 100644 --- a/src/ui/sidebar/HostManagerTabs.tsx +++ b/src/ui/sidebar/HostManagerTabs.tsx @@ -4,7 +4,7 @@ import { KeyRound, Settings, SquareTerminal, Terminal } from "lucide-react"; import { byOrderThenId, createRegistry } from "@/lib/registry"; /** Core host editor tabs. Plugins add theirs through registerHostEditorSection. */ -export type CoreHostTabId = "general" | "ssh" | "terminal"; +export type CoreHostTabId = "general" | "ssh" | "terminal-options"; export type HostTabId = CoreHostTabId | (string & {}); export type CredentialTabId = "general" | "auth"; @@ -60,7 +60,7 @@ export const hostEditorSectionList = sections.list; export const useHostEditorSections = sections.useList; export const resetHostEditorSections = sections.reset; -const CORE_SSH_GROUP = new Set<string>(["ssh", "terminal"]); +const CORE_SSH_GROUP = new Set<string>(["ssh", "terminal-options"]); /** Whether a tab lives in the SSH group's second strip. */ export function isSshGroupTab(id: string): boolean { @@ -148,7 +148,7 @@ export function makeHostSshSubTabs( order: 0, }, { - id: "terminal", + id: "terminal-options", label: t("hosts.tabTerminal"), icon: <SquareTerminal className="size-3" />, order: 10, diff --git a/src/ui/sidebar/HostsPanel.tsx b/src/ui/sidebar/HostsPanel.tsx index 48dcd9b72..f85f53ec7 100644 --- a/src/ui/sidebar/HostsPanel.tsx +++ b/src/ui/sidebar/HostsPanel.tsx @@ -151,7 +151,6 @@ function hostPassesFilters(host: Host, filters: FilterState): boolean { } if (filters.features.length > 0) { const ok = - (filters.features.includes("terminal") && host.enableTerminal) || (filters.features.includes("fileManager") && host.enableFileManager) || (filters.features.includes("docker") && host.enableDocker); if (!ok) return false; @@ -794,7 +793,6 @@ export function HostsPanel({ </DropdownMenuLabel> {( [ - ["terminal", "Terminal"], ["fileManager", "FileManager"], ["docker", "Docker"], ] as const diff --git a/src/ui/sidebar/QuickConnectPanel.tsx b/src/ui/sidebar/QuickConnectPanel.tsx index 83c113531..a9839ea17 100644 --- a/src/ui/sidebar/QuickConnectPanel.tsx +++ b/src/ui/sidebar/QuickConnectPanel.tsx @@ -76,7 +76,8 @@ export function QuickConnectPanel({ onConnect }: QuickConnectPanelProps) { protocol, domain: domain || undefined, }); - onConnect(hostConfig, type ?? resolveHostTabType(hostConfig)); + const target = type ?? resolveHostTabType(hostConfig); + if (target) onConnect(hostConfig, target); }; const connectDefault = () => connect(); @@ -282,7 +283,7 @@ export function QuickConnectPanel({ onConnect }: QuickConnectPanelProps) { ) : ( <> <button - onClick={() => connect("terminal")} + onClick={() => connect()} className="flex items-center justify-center gap-1.5 h-7 w-full border border-accent-brand/40 bg-accent-brand/10 text-accent-brand text-xs font-semibold hover:bg-accent-brand/20 transition-colors" > <Terminal className="size-3.5" /> diff --git a/src/ui/sidebar/rail-items.ts b/src/ui/sidebar/rail-items.ts index 1cc423a9a..339a42532 100644 --- a/src/ui/sidebar/rail-items.ts +++ b/src/ui/sidebar/rail-items.ts @@ -1,6 +1,5 @@ import { Braces, - Clock, Fingerprint, Hammer, KeyRound, @@ -9,7 +8,6 @@ import { ScrollText, Server, Settings, - TerminalSquare, Usb, User, Zap, @@ -124,14 +122,7 @@ export const RAIL_ITEMS: RailItemDef[] = [ promotable: true, rightDockable: true, }, - { - id: "history", - icon: Clock, - labelKey: "nav.history", - separatorAfter: true, - promotable: true, - rightDockable: true, - }, + { id: "session-logs", icon: ScrollText, @@ -146,14 +137,6 @@ export const RAIL_ITEMS: RailItemDef[] = [ labelKey: "nav.splitScreen", separatorAfter: true, }, - { - id: "local-terminal", - icon: TerminalSquare, - labelKey: "nav.localTerminal", - kind: "tab", - separatorAfter: true, - electronOnly: true, - }, ]; /** diff --git a/src/ui/sidebar/tree/HostItem/HostItem.tsx b/src/ui/sidebar/tree/HostItem/HostItem.tsx index 7f8c7f541..ab595a966 100644 --- a/src/ui/sidebar/tree/HostItem/HostItem.tsx +++ b/src/ui/sidebar/tree/HostItem/HostItem.tsx @@ -126,7 +126,6 @@ export function getSshActions(host: Host): { return [ // --- tmux-monitor --- opt-in per host, off by default host.enableSsh && - host.enableTerminal && host.enableTmuxMonitor && { type: "tmux_monitor" as TabType, icon: Layers, @@ -419,8 +418,9 @@ export function HostItem({ action.tabType ? [action.tabType] : [], ), ]; + // Empty when no running plugin can connect to this host. const defaultAction: TabType = - defaultConnectAction(allHostActions, host)?.tabType ?? "terminal"; + defaultConnectAction(allHostActions, host)?.tabType ?? ""; const openHostTab = ( type: TabType, options?: { @@ -429,6 +429,7 @@ export function HostItem({ forceNewTab?: boolean; }, ) => { + if (!type) return; markTabSurfaceUsed(type); recordHostActionPreference(host.id, type); onOpenTab(type, { @@ -798,22 +799,20 @@ export function HostItem({ {t("hosts.copyLink")} </DropdownMenuSubTrigger> <DropdownMenuSubContent> - {host.enableSsh && - host.enableTerminal && - 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> - )} + {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) => { @@ -972,7 +971,7 @@ export function HostItem({ }} style={depthStyle} onPointerEnter={() => { - preloadTabSurface(defaultAction); + if (defaultAction) preloadTabSurface(defaultAction); const preferredAction = getPreferredHostAction( host.id, availableActions, diff --git a/src/ui/sidebar/tree/SidebarTree.tsx b/src/ui/sidebar/tree/SidebarTree.tsx index 7404343b0..e4a4af133 100644 --- a/src/ui/sidebar/tree/SidebarTree.tsx +++ b/src/ui/sidebar/tree/SidebarTree.tsx @@ -447,7 +447,7 @@ export function SidebarTree({ const hosts = collectAllHosts(folder.children); for (const host of hosts) { const type = resolveHostTabType(host); - onOpenTab(host, type); + if (type) onOpenTab(host, type); } } @@ -1285,7 +1285,8 @@ export function SidebarTree({ selectedHostIds.has(String(h.id)), ); for (const host of selectedHosts) { - onOpenTab(host, resolveHostTabType(host)); + const type = resolveHostTabType(host); + if (type) onOpenTab(host, type); } setSelectedHostIds(new Set()); onToggleSelectionMode(); diff --git a/src/ui/tests/features/terminal/terminal-font-zoom.test.ts b/src/ui/tests/lib/terminal-look/terminal-font-zoom.test.ts similarity index 97% rename from src/ui/tests/features/terminal/terminal-font-zoom.test.ts rename to src/ui/tests/lib/terminal-look/terminal-font-zoom.test.ts index 4e2135498..35705507d 100644 --- a/src/ui/tests/features/terminal/terminal-font-zoom.test.ts +++ b/src/ui/tests/lib/terminal-look/terminal-font-zoom.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest"; import { getNextTerminalFontSize, getTerminalFontZoomDirection, -} from "../../../features/terminal/terminal-font-zoom"; +} from "@/lib/terminal-look/terminal-font-zoom"; function keyEvent( overrides: Partial<KeyboardEvent>, diff --git a/src/ui/tests/features/terminal/terminal-global-styles.test.ts b/src/ui/tests/lib/terminal-look/terminal-global-styles.test.ts similarity index 95% rename from src/ui/tests/features/terminal/terminal-global-styles.test.ts rename to src/ui/tests/lib/terminal-look/terminal-global-styles.test.ts index bce2b0e2b..11df15a89 100644 --- a/src/ui/tests/features/terminal/terminal-global-styles.test.ts +++ b/src/ui/tests/lib/terminal-look/terminal-global-styles.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from "vitest"; -import { ensureTerminalFontsLoaded } from "../../../features/terminal/terminal-global-styles"; +import { ensureTerminalFontsLoaded } from "@/lib/terminal-look/terminal-global-styles"; describe("ensureTerminalFontsLoaded", () => { it("requests regular, bold, italic, and bold-italic variants for the given font", () => { diff --git a/src/ui/tests/plugin-host/registries.test.tsx b/src/ui/tests/plugin-host/registries.test.tsx index b31637e93..b9bd23ffd 100644 --- a/src/ui/tests/plugin-host/registries.test.tsx +++ b/src/ui/tests/plugin-host/registries.test.tsx @@ -225,7 +225,11 @@ describe("registries through the app object", () => { it("adds a host editor section to the SSH group in order", async () => { await mount(); const ids = makeHostSshSubTabs((key) => key).map((tab) => tab.id); - expect(ids.slice(0, 3)).toEqual(["ssh", "terminal", "fixture-section"]); + expect(ids.slice(0, 3)).toEqual([ + "ssh", + "terminal-options", + "fixture-section", + ]); expect(makeHostTabs((key) => key).map((tab) => tab.id)).not.toContain( "fixture-section", ); diff --git a/src/ui/tests/plugin-host/view-ownership.test.tsx b/src/ui/tests/plugin-host/view-ownership.test.tsx index df17e6953..604337141 100644 --- a/src/ui/tests/plugin-host/view-ownership.test.tsx +++ b/src/ui/tests/plugin-host/view-ownership.test.tsx @@ -31,7 +31,6 @@ const shell = { renameTab: () => {}, openFileInEditor: () => {}, openFileManager: () => {}, - openTerminalTab: () => {}, openRailView: () => {}, closeRailView: () => {}, }; diff --git a/src/ui/tests/sidebar/AdminTouchInputSection.test.tsx b/src/ui/tests/sidebar/AdminTouchInputSection.test.tsx deleted file mode 100644 index 797ec584d..000000000 --- a/src/ui/tests/sidebar/AdminTouchInputSection.test.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; -import { render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { AdminTouchInputSection } from "../../sidebar/AdminTouchInputSection"; -import { TOUCH_INPUT_DEFAULTS } from "@/types/touch-input-settings"; - -vi.mock("react-i18next", () => ({ - useTranslation: () => ({ t: (key: string) => key }), -})); - -describe("AdminTouchInputSection", () => { - it("shows simple defaults, keeps tunables collapsed, and resets", async () => { - const user = userEvent.setup(); - const onReset = vi.fn(); - render( - <AdminTouchInputSection - open - onToggle={vi.fn()} - settings={{ ...TOUCH_INPUT_DEFAULTS }} - setSettings={vi.fn()} - onSave={vi.fn()} - onReset={onReset} - />, - ); - - expect(screen.getByText("admin.touchEnabled")).toBeTruthy(); - expect(screen.queryByLabelText("admin.touchDragThreshold")).toBeNull(); - await user.click(screen.getByText("admin.touchAdvanced")); - expect( - (screen.getByLabelText("admin.touchDragThreshold") as HTMLInputElement) - .value, - ).toBe("6"); - expect( - ( - screen.getByLabelText( - "admin.touchMaximumTicksPerFrame", - ) as HTMLInputElement - ).value, - ).toBe("4"); - await user.click(screen.getByText("admin.touchResetDefaults")); - expect(onReset).toHaveBeenCalledOnce(); - }); -}); diff --git a/src/ui/tests/sidebar/rail-items.test.ts b/src/ui/tests/sidebar/rail-items.test.ts index f40eb6057..d4af1668f 100644 --- a/src/ui/tests/sidebar/rail-items.test.ts +++ b/src/ui/tests/sidebar/rail-items.test.ts @@ -60,10 +60,8 @@ describe("RAIL_ITEMS", () => { "serial", "ssh-tools", "macros", - "history", "session-logs", "split-screen", - "local-terminal", ]); }); @@ -84,7 +82,7 @@ describe("RAIL_ITEMS", () => { [...RAIL_ITEMS, ...RAIL_UTILITY_ITEMS] .filter((item) => item.promotable) .map((item) => item.id), - ).toEqual(["termix-id", "ssh-tools", "macros", "history", "session-logs"]); + ).toEqual(["termix-id", "ssh-tools", "macros", "session-logs"]); }); it("derives promotableIds from the promotable flag", () => { @@ -125,7 +123,6 @@ describe("RAIL_ITEMS", () => { "connections", "ssh-tools", "macros", - "history", "session-logs", ]); }); @@ -198,15 +195,27 @@ describe("railItemLabel", () => { delete (window as { IS_ELECTRON?: boolean }).IS_ELECTRON; }); + const desktopOnly = { + id: "desktop-only", + icon: Boxes, + labelKey: "nav.desktopOnly", + pluginId: "p", + electronOnly: true, + }; + it("hides electron-only destinations in the browser build", () => { + const dispose = registerRailItem(desktopOnly); const ids = visibleRailItems().map((item) => item.id); - expect(ids).not.toContain("local-terminal"); + dispose(); + expect(ids).not.toContain("desktop-only"); }); it("shows electron-only destinations in the desktop app", () => { (window as { IS_ELECTRON?: boolean }).IS_ELECTRON = true; + const dispose = registerRailItem(desktopOnly); const ids = visibleRailItems().map((item) => item.id); - expect(ids).toContain("local-terminal"); + dispose(); + expect(ids).toContain("desktop-only"); }); it("keeps every non-electron item in both builds", () => { diff --git a/src/ui/tests/sidebar/tree/HostItem/HostItem.test.tsx b/src/ui/tests/sidebar/tree/HostItem/HostItem.test.tsx index da7c71334..2a088b639 100644 --- a/src/ui/tests/sidebar/tree/HostItem/HostItem.test.tsx +++ b/src/ui/tests/sidebar/tree/HostItem/HostItem.test.tsx @@ -229,7 +229,8 @@ describe("HostItem density parity", () => { expect(hostRow).toBeTruthy(); fireEvent.pointerEnter(hostRow!); - expect(preloadTabSurfaceMock).toHaveBeenCalledWith("terminal"); + // No plugin registered a connect action, so only the learned tool preloads. + expect(preloadTabSurfaceMock).not.toHaveBeenCalledWith(""); expect(preloadTabSurfaceMock).toHaveBeenCalledWith("tmux_monitor"); }); });