diff --git a/docker/nginx-https.conf b/docker/nginx-https.conf index 83a7b5aa5..21a831f8c 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-don5VKNqA8VQplPO1j5zyOVDdbTQrgGSSeVPwJ7b0Lo='; 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-teIJ2CKVs77AjLM0kquz3mSdDahr9QtMZGG0tQjbbkU='; 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; diff --git a/docker/nginx.conf b/docker/nginx.conf index 721d11438..5d3354b07 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-don5VKNqA8VQplPO1j5zyOVDdbTQrgGSSeVPwJ7b0Lo='; 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-teIJ2CKVs77AjLM0kquz3mSdDahr9QtMZGG0tQjbbkU='; 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; diff --git a/drizzle/mysql/0043_unusual_lucky_pierre.sql b/drizzle/mysql/0043_unusual_lucky_pierre.sql new file mode 100644 index 000000000..500ad74cf --- /dev/null +++ b/drizzle/mysql/0043_unusual_lucky_pierre.sql @@ -0,0 +1,9 @@ +-- host_metrics_preferences, host_health_checks, host_health_history and +-- host_metrics_history moved to the host-metrics plugin, which adopts them by +-- rename in its own first migration. That runs after this one, so a real +-- DROP TABLE here would delete every row first. +SELECT 1;--> statement-breakpoint +-- Status checks are core: the per-host switch and interval, copied out of +-- stats_config at boot by host-status-config-migration.ts. +ALTER TABLE `ssh_data` ADD `status_check_enabled` boolean DEFAULT true NOT NULL;--> statement-breakpoint +ALTER TABLE `ssh_data` ADD `status_check_interval` int; \ No newline at end of file diff --git a/drizzle/mysql/0044_jittery_thunderball.sql b/drizzle/mysql/0044_jittery_thunderball.sql new file mode 100644 index 000000000..41cc0bf73 --- /dev/null +++ b/drizzle/mysql/0044_jittery_thunderball.sql @@ -0,0 +1,6 @@ +-- ssh_data.stats_config split: its status half moved to status_check_enabled +-- and status_check_interval, its metrics half to the host-metrics plugin's +-- host settings. Both copies run at boot, after this migration, so a real +-- DROP COLUMN here would delete the data first. The column stays in place, +-- unused, until 3.0.0. +SELECT 1; diff --git a/drizzle/mysql/meta/0043_snapshot.json b/drizzle/mysql/meta/0043_snapshot.json new file mode 100644 index 000000000..8a4176527 --- /dev/null +++ b/drizzle/mysql/meta/0043_snapshot.json @@ -0,0 +1,7221 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "2d7713db-3755-42e4-9a03-7083c7a82344", + "prevId": "3ab7c6d2-4f87-483a-a0bf-117aa000c3c8", + "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": {} + }, + "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_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 + }, + "status_check_enabled": { + "name": "status_check_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "status_check_interval": { + "name": "status_check_interval", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "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 + }, + "ssh_port": { + "name": "ssh_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "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 + }, + "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 + }, + "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": {} + }, + "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": {} + }, + "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 + }, + "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/0044_snapshot.json b/drizzle/mysql/meta/0044_snapshot.json new file mode 100644 index 000000000..06cbc248f --- /dev/null +++ b/drizzle/mysql/meta/0044_snapshot.json @@ -0,0 +1,7214 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "bf0c51de-8d0f-4d1d-8270-a6661aa24d8a", + "prevId": "2d7713db-3755-42e4-9a03-7083c7a82344", + "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": {} + }, + "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_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 + }, + "status_check_enabled": { + "name": "status_check_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "status_check_interval": { + "name": "status_check_interval", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "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 + }, + "ssh_port": { + "name": "ssh_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "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 + }, + "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 + }, + "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": {} + }, + "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": {} + }, + "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 + }, + "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 c9224f663..aa0ea3ff3 100644 --- a/drizzle/mysql/meta/_journal.json +++ b/drizzle/mysql/meta/_journal.json @@ -302,6 +302,20 @@ "when": 1790227893950, "tag": "0042_colorful_bill_hollister", "breakpoints": true + }, + { + "idx": 43, + "version": "5", + "when": 1790266379295, + "tag": "0043_unusual_lucky_pierre", + "breakpoints": true + }, + { + "idx": 44, + "version": "5", + "when": 1790266402855, + "tag": "0044_jittery_thunderball", + "breakpoints": true } ] } \ No newline at end of file diff --git a/drizzle/postgres/0044_nostalgic_omega_sentinel.sql b/drizzle/postgres/0044_nostalgic_omega_sentinel.sql new file mode 100644 index 000000000..15c298d97 --- /dev/null +++ b/drizzle/postgres/0044_nostalgic_omega_sentinel.sql @@ -0,0 +1,9 @@ +-- host_metrics_preferences, host_health_checks, host_health_history and +-- host_metrics_history moved to the host-metrics plugin, which adopts them by +-- rename in its own first migration. That runs after this one, so a real +-- DROP TABLE here would delete every row first. +SELECT 1;--> statement-breakpoint +-- Status checks are core: the per-host switch and interval, copied out of +-- stats_config at boot by host-status-config-migration.ts. +ALTER TABLE "ssh_data" ADD COLUMN "status_check_enabled" boolean DEFAULT true NOT NULL;--> statement-breakpoint +ALTER TABLE "ssh_data" ADD COLUMN "status_check_interval" integer; \ No newline at end of file diff --git a/drizzle/postgres/0045_thankful_shinobi_shaw.sql b/drizzle/postgres/0045_thankful_shinobi_shaw.sql new file mode 100644 index 000000000..41cc0bf73 --- /dev/null +++ b/drizzle/postgres/0045_thankful_shinobi_shaw.sql @@ -0,0 +1,6 @@ +-- ssh_data.stats_config split: its status half moved to status_check_enabled +-- and status_check_interval, its metrics half to the host-metrics plugin's +-- host settings. Both copies run at boot, after this migration, so a real +-- DROP COLUMN here would delete the data first. The column stays in place, +-- unused, until 3.0.0. +SELECT 1; diff --git a/drizzle/postgres/meta/0044_snapshot.json b/drizzle/postgres/meta/0044_snapshot.json new file mode 100644 index 000000000..c5ef25c3b --- /dev/null +++ b/drizzle/postgres/meta/0044_snapshot.json @@ -0,0 +1,7054 @@ +{ + "id": "4c088e15-1813-4c0c-88ad-1cdb8943c0a7", + "prevId": "6a036d52-3dd7-4fe7-aec5-5dbc5244b4f5", + "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.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_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 + }, + "status_check_enabled": { + "name": "status_check_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "status_check_interval": { + "name": "status_check_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "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 + }, + "ssh_port": { + "name": "ssh_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 22 + }, + "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 + }, + "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 + }, + "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.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.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 + }, + "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/0045_snapshot.json b/drizzle/postgres/meta/0045_snapshot.json new file mode 100644 index 000000000..d3f0f405e --- /dev/null +++ b/drizzle/postgres/meta/0045_snapshot.json @@ -0,0 +1,7048 @@ +{ + "id": "261179bf-2c06-49d0-b6a8-062fd981c149", + "prevId": "4c088e15-1813-4c0c-88ad-1cdb8943c0a7", + "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.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_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 + }, + "status_check_enabled": { + "name": "status_check_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "status_check_interval": { + "name": "status_check_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "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 + }, + "ssh_port": { + "name": "ssh_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 22 + }, + "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 + }, + "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 + }, + "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.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.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 + }, + "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 ae963d2cb..aa33abc9a 100644 --- a/drizzle/postgres/meta/_journal.json +++ b/drizzle/postgres/meta/_journal.json @@ -309,6 +309,20 @@ "when": 1790227893380, "tag": "0043_melodic_terror", "breakpoints": true + }, + { + "idx": 44, + "version": "7", + "when": 1790266378802, + "tag": "0044_nostalgic_omega_sentinel", + "breakpoints": true + }, + { + "idx": 45, + "version": "7", + "when": 1790266402403, + "tag": "0045_thankful_shinobi_shaw", + "breakpoints": true } ] } \ No newline at end of file diff --git a/drizzle/sqlite/0040_rich_zuras.sql b/drizzle/sqlite/0040_rich_zuras.sql new file mode 100644 index 000000000..7575d092f --- /dev/null +++ b/drizzle/sqlite/0040_rich_zuras.sql @@ -0,0 +1,9 @@ +-- host_metrics_preferences, host_health_checks, host_health_history and +-- host_metrics_history moved to the host-metrics plugin, which adopts them by +-- rename in its own first migration. That runs after this one, so a real +-- DROP TABLE here would delete every row first. +SELECT 1;--> statement-breakpoint +-- Status checks are core: the per-host switch and interval, copied out of +-- stats_config at boot by host-status-config-migration.ts. +ALTER TABLE `ssh_data` ADD `status_check_enabled` integer DEFAULT true NOT NULL;--> statement-breakpoint +ALTER TABLE `ssh_data` ADD `status_check_interval` integer; \ No newline at end of file diff --git a/drizzle/sqlite/0041_nosy_mystique.sql b/drizzle/sqlite/0041_nosy_mystique.sql new file mode 100644 index 000000000..41cc0bf73 --- /dev/null +++ b/drizzle/sqlite/0041_nosy_mystique.sql @@ -0,0 +1,6 @@ +-- ssh_data.stats_config split: its status half moved to status_check_enabled +-- and status_check_interval, its metrics half to the host-metrics plugin's +-- host settings. Both copies run at boot, after this migration, so a real +-- DROP COLUMN here would delete the data first. The column stays in place, +-- unused, until 3.0.0. +SELECT 1; diff --git a/drizzle/sqlite/meta/0040_snapshot.json b/drizzle/sqlite/meta/0040_snapshot.json new file mode 100644 index 000000000..adecede92 --- /dev/null +++ b/drizzle/sqlite/meta/0040_snapshot.json @@ -0,0 +1,6808 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "c42443e2-beb6-4345-b4c4-2c761afc862a", + "prevId": "0e13985e-2be2-487e-9aaa-510357a7dcb9", + "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": {} + }, + "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_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 + }, + "status_check_enabled": { + "name": "status_check_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "status_check_interval": { + "name": "status_check_interval", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "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 + }, + "ssh_port": { + "name": "ssh_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "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 + }, + "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 + }, + "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": {} + }, + "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": {} + }, + "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 + }, + "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/0041_snapshot.json b/drizzle/sqlite/meta/0041_snapshot.json new file mode 100644 index 000000000..436ae4987 --- /dev/null +++ b/drizzle/sqlite/meta/0041_snapshot.json @@ -0,0 +1,6801 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "18dc3df0-32ee-41c0-829d-be744fa90225", + "prevId": "c42443e2-beb6-4345-b4c4-2c761afc862a", + "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": {} + }, + "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_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 + }, + "status_check_enabled": { + "name": "status_check_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "status_check_interval": { + "name": "status_check_interval", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "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 + }, + "ssh_port": { + "name": "ssh_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "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 + }, + "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 + }, + "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": {} + }, + "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": {} + }, + "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 + }, + "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 d0d7d1ab6..20745865d 100644 --- a/drizzle/sqlite/meta/_journal.json +++ b/drizzle/sqlite/meta/_journal.json @@ -281,6 +281,20 @@ "when": 1790227892787, "tag": "0039_lively_impossible_man", "breakpoints": true + }, + { + "idx": 40, + "version": "6", + "when": 1790266378262, + "tag": "0040_rich_zuras", + "breakpoints": true + }, + { + "idx": 41, + "version": "6", + "when": 1790266401908, + "tag": "0041_nosy_mystique", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/plugin-sdk/ARCHITECTURE.md b/packages/plugin-sdk/ARCHITECTURE.md index 1759a3f39..b67017eb0 100644 --- a/packages/plugin-sdk/ARCHITECTURE.md +++ b/packages/plugin-sdk/ARCHITECTURE.md @@ -466,8 +466,12 @@ 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 `shell.overlay`. **B12** added `shell.overlay`, a component slot the +`onboarding.workflow`, `hosts.importMenu`, `hosts.panel`, `proxmox.hostEditor`, +`shell.overlay`, `dashboard.hostMetrics` and `homepage.hostMetrics`. **B16** +added the last two, component slots in the dashboard's host status card +(`{ hostId, online }`, one per host row) and the homepage's host status widget +(`{ hostId, shownMetrics, online }`): core draws the status and a plugin draws +the numbers, so core no longer calls the metrics API. **B12** added `shell.overlay`, a component slot the shell renders once at its root for always-mounted plugin UI: session-sharing keeps its share dialog and its room-invite watcher there. `session.remoteDisplay` moved to session-sharing, which declares it and draws @@ -482,6 +486,9 @@ or without tailscale enabled. Cross-plugin frontend calls go through actions: 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. +**B16** added `host-metrics.disk` (the file manager's disk usage bar) and +`snippets.get` / `snippets.execute` (host-metrics's quick actions); each +answers undefined while its plugin is off. **B9** moved the terminal itself into ssh-terminal, which adds `terminal.open(host, { path?, joinSharedSessionId?, joinShareId?, label? })` @@ -571,6 +578,11 @@ connection helpers (`isElectron`, `resolveConnectionOrigin`, `pluginWsUrl`, (`logActivity`, `getHostPassword`, `patchOpenTab`, `getUserPreferences`, `parseCustomKeybindings`, `setHostAutoTmux`, `getCookie`). The APIs are a stopgap in the right place: D1 turns them into typed bridge members. +**B16** added `LineChart`, `useAdaptivePolling`, `useAreaPreferences` (the +Appearance density and chart options), and the homepage widget pieces +`WidgetTitle` and `runVisibleInterval`, for host-metrics's cards and its +metrics chart widget. The frontend SDK gained `useHostStatus(hostId)`: core's +status for a host, from the shell's own polling, or null outside the shell. **B14** added `buildOriginWsUrl`, `getBasePath`, `resolveRemoteHostId` and the `ConnectionStage` type, for remote desktop's display socket and its remote host lookup. `useConnectionDefaults` now carries terminal defaults only; the @@ -696,6 +708,44 @@ the `shareable`, `canShare`, `openShareModal` and `getShareTarget` members of `TabOptions`/`TabHandle` are gone. Core's active connections list gets the sessions shared with a user from the `sessions.sharedWithMe` action. +#### Host status is core + +The status dot in the host list, the dashboard and the homepage widget is +core, in `src/backend/hosts/status/`. **B16** moved it out of host-metrics's +poller. Core opens a TCP connection to every host with status checks on +(`status_check_enabled`, a core host column), on the host's own interval +(`status_check_interval`) or the admin default (`global_status_check_interval` +under `GET/PUT /host/status/settings`), through core's jump host chain when the +host has one. It never logs in: on RADIUS or Duo backed devices a login fires +a real 2FA push. That gives "reachable" or "offline". A host is "online" when +a login worked: an open session (`ctx.hosts.trackSession`) or a plugin's +`ctx.hosts.status.reportLogin`. Polling is demand driven: a user asking for +`GET /host/status` starts their own hosts. Core emits `host.status` +(`{ hostId, ownerUserId, status, previous, online, reason? }`) when a status +changes, and `host.key.updated` (`{ hostId }`) when a changed host key is +accepted. The status half of the old `stats_config` moved into the two columns +(`host-status-config-migration.ts`); its metrics half is host-metrics's. + +**Host metrics (B16)** polls only hosts somebody is looking at (a tab, a +dashboard row, a homepage widget, or another plugin through the +`host-metrics.viewers` service, v1: `register(hostId)`, `heartbeat`, +`unregister`, all as the caller). It samples over `ctx.ssh` pools and timers +from `ctx.schedule`, keeps the adaptive metrics concurrency (one worker per 20 +polled hosts, 5 to 50, `METRICS_POLL_CONCURRENCY` overrides), and waits for +core to call a host reachable before the first heavy sample. It emits +`plugin.host-metrics.snapshot` (`{ hostId, ownerUserId, metrics }`) for every +sample, `plugin.host-metrics.status` (`{ hostId, ownerUserId, state, +previous }`, state `collecting`, `auth_failed`, `host_key_changed` or +`unreachable`) when collection changes, and `plugin.host-metrics.health-check` +(`{ hostId, userId, checkId, ok, detail? }`) for each health check result; +automations subscribes to them. A plugin adds data to every sample by +providing `host-metrics.collectors` (v1: `collect(client, hostId)`) under its +own provider name; the result lands under `extra[]`. Proxmox does not: +its stats come from the Proxmox API, not from SSH on the host. The intervals, +history retention and the new-host default are admin settings, the +temperature unit a user setting, and the rest of `stats_config` host +settings. + ### 10. Lifecycle `activate(ctx)` creates all state. Everything registered through `ctx` or `app` @@ -999,7 +1049,11 @@ the database, unused, until 3.0.0 removes it. A copy reads it with raw SQL through `utils/crypto-migration/raw-rows.ts`, whose `selectRows` and `runStatement` work on all three engines: `getDb().all()` exists only on SQLite. Remote desktop's host options and `user_preferences.rdp_defaults` are -the first columns handled this way. +the first columns handled this way. **B16** split `stats_config` the same way, into +two core columns and host-metrics's host settings. Adding columns and +dropping one in the same drizzle-kit run makes it ask whether that is a +rename, which it cannot do without a terminal, so B16 generated two +migrations: the added columns first, then the no-op drop. --- @@ -1266,12 +1320,13 @@ Built per plugin in `src/backend/plugins/ctx.ts` and passed to `activate`. | `ctx.asUser(userId, fn)` | none, always audited | **A1** | | `ctx.currentActor()` | none | **A1** | | `ctx.db.define` / `.client` / `.refs` | `db:own` | **A3** | -| `ctx.db.persist` / `.dialect` | `db:own` (persist only) | **A9** | +| `ctx.db.persist` / `.dialect` | `db:own` (persist only) | **A9**, lazy **B16** | | `ctx.sync.registerEntity` | none | **A3** | | `ctx.http.router` / `ctx.ws.route` / `.upgrade` | `network:serve` | **A4** | | `ctx.rbac.has` / `.hasFor` / `.require` | own permissions only | **A5** | | `ctx.capabilities.has` / `.require` | the capability itself | **B11** | | `ctx.hosts.*` | `hosts:read` / `hosts:write` | **B4**, extended **B5** | +| `ctx.hosts.status.*` | `hosts:read` | **B16** | | `ctx.ssh.*` | `ssh:connect`, `credentials:use` | **A8** | | `ctx.settings.*` | `settings:read-core` (readCore only) | **A6** | | `ctx.notify.*` | `notify:send` | A6 | @@ -1280,6 +1335,7 @@ Built per plugin in `src/backend/plugins/ctx.ts` and passed to `activate`. | `ctx.desktop.launchNativeRdp` / `.available` | `desktop:window` (launch only) | **B14** | | `ctx.credentials.resolveHostProtocol` | `credentials:read` | **B14** | | `ctx.audit.record` | none, the actor is the runtime's | **B9** | +| `ctx.schedule.every` / `.after` | none | **B16** | | `ctx.fetch` | `network:outbound` | B | **B11** added `ctx.capabilities.has(capability)` / `.require(capability)`, a @@ -1431,6 +1487,39 @@ resolution (the owner's shared snapshot or their own override), never the owner's raw secret; no connect access, or no host, is `null`. Every call is audited as `plugin_credentials_read`, allowed or refused. +**B16** added `ctx.hosts.status`, the plugin side of core's host status +check (see [Host status](#host-status-is-core)): + +- `get(hostId)` is core's last result, or `null` before the first check or + for a host with status checks off. `check(hostId)` checks now unless the + last result is under 30 seconds old. host-metrics calls it before the first + heavy sample of a host. +- `reportLogin(hostId, { ok, hostKeyChanged? })` tells core a login to the + host worked or failed. A working login is what turns "reachable" into + "online". Synchronous and not audited, like `trackSession`: a poller calls + it on every sample. +- `registerPort(connectionType, resolve)` tells core which port proves a host + of that connection type is up, for a protocol whose port is the plugin's own + host setting. remote-desktop registers `rdp`, `vnc` and `telnet`; without + it core checks the host's port. Removed on deactivate. + +**B16** also added `ctx.ssh.dropPooled(pool, hostId)`, which closes this +plugin's pooled connections to one host (host-metrics calls it on +`host.updated`). It only touches keys this plugin created, so, like +deactivate, it never closes another plugin's connections. The core pool is +never cleared as a whole by a plugin. + +**B16** added `ctx.schedule`: `every(ms, fn, { jitterMs?, runNow? })` and +`after(ms, fn)`, each returning a function that stops it. Timers live in the +plugin's disposable bag, so deactivate clears them; a run that throws is +logged, and a repeating job skips a tick while its last run is still going. +`jitterMs` delays the first run by a random amount up to that, so a fleet of +per-host timers spreads out. Needs no capability. + +`ctx.db.persist({ lazy: true })` (**B16**) marks the database dirty and lets +the debounced save write it, for frequent low-value writes such as metrics +samples; the default still saves before returning. + `ctx.hosts.create` (**B14**) hands the created row to every plugin's `hostImportNormalizer`, the same as a bulk import row, so a plugin creating a host can set another plugin's host settings (proxmox marks an imported @@ -1961,7 +2050,8 @@ Then, with the app running: ## Legacy core imports: the debt D1 removes The bundled plugins predate the SDK, apart from workspaces (A9), snippets -(B2) and remote-desktop (B14), which import nothing from core. The others still reach core by relative +(B2), remote-desktop (B14) and host-metrics (B16), which import nothing from +core. The others still reach core by relative path (`../../../../src/backend/...`), which an esbuild plugin, `packages/plugin-sdk/cli/lib/legacy-core-imports.mjs`, keeps out of the bundle and rewrites to the compiled output path (`../../../backend/backend/...`, or @@ -1988,9 +2078,9 @@ 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 | 120 files | D1 | -| A plugin importing core by relative path | Warning | 63 files | D1 | -| A plugin importing another plugin's source | Warning | 3 files | B18 | +| A plugin frontend importing core through `@/` | Warning | 84 files | D1 | +| A plugin importing core by relative path | Warning | 29 files | D1 | +| A plugin importing another plugin's source | Warning | 1 file | B18 | A warning does not fail a build, so the counts are held by `scripts/check-plugin-boundaries.cjs`, run by `npm run lint`. Every offender is @@ -2012,12 +2102,12 @@ Known specifics: automations is disabled. In B18 `ai` calls automations through an `automations` service as an `optionalDependency` and the direct import goes away. -- **TODO(B18):** `plugins/host-metrics/src/backend/` reaches `automations` - twice, for a `MetricsSnapshot` type and a fire-and-forget - `import("...headless-viewer.js")` that registers a viewer bridge. Both - degrade silently when automations is absent, so host-metrics declares it as - an `optionalDependency` (added in A2; it previously declared nothing, and - the loader had no reason to order the two). +- **B16** moved host-metrics onto the SDK: it imports nothing from core or + automations. Automations reaches it through the `host-metrics.viewers` + service and its `plugin.host-metrics.*` events, and core reaches it through + the `dashboard.hostMetrics` and `homepage.hostMetrics` slots and the + `host-metrics.disk` action. Core's `hosts/metrics-shared/` is gone: ai, + automations, proxmox and tailscale use `@termix/plugin-sdk/host-commands`. - Core feature servers that are not plugins yet still own ports: dashboard 30006 and homepage 30012. Each keeps its nginx block until its own Phase B step. No plugin owns a port any more (A4). **B6** moved file-manager off diff --git a/packages/plugin-sdk/FINISH-LIST.md b/packages/plugin-sdk/FINISH-LIST.md index 46a47ddc0..9fa377f31 100644 --- a/packages/plugin-sdk/FINISH-LIST.md +++ b/packages/plugin-sdk/FINISH-LIST.md @@ -10,29 +10,15 @@ it's done. shared with host-metrics's originals. Promote a shared version into `@termix/plugin-sdk/ui` once a second manager-card plugin needs one, per the contract's "add to the SDK when a second plugin needs it" rule. Owner: D1. -- **B3 (tailscale):** `plugins/tailscale/src/backend/host-metrics-manager.ts` - and `routes.ts` still resolve hosts and access through core's - `PermissionManager`/`resolveHostById`/`DataCrypto` by relative import - instead of `ctx.hosts`, which **B4** built. Same debt every other - host-metrics manager already carries; not new, but tailscale now carries it - too. Owner: host-metrics's own Phase B step (it and every manager plugin - convert together). -- **B4 (fleets/host-metrics):** `src/backend/hosts/metrics-shared/` (platform - detection, package commands, exec-elevated) is still core's own copy, used - by host-metrics, which has not converted yet. `@termix/plugin-sdk/host-commands` - is the SDK copy fleets now uses; core's copy was left in place rather than - turned into a re-export, since host-metrics's own conversion step is where - its imports should move wholesale. Owner: host-metrics's Phase B step. - **B5 (proxmox):** `runAdaptivePolling` and `cn` are duplicated in `plugins/proxmox/src/frontend/stats/` rather than shared with core's `src/ui/lib/` originals. `useConnectionRetry` (the third duplicate B5 left here) is resolved: **B6** promoted it into `@termix/plugin-sdk/frontend` once file-manager became its fourth caller (after proxmox, docker, host-metrics, remote-desktop) and switched proxmox's own copy over too. - host-metrics's own conversion needs `runAdaptivePolling` for its stats tab, - so promote it into `@termix/plugin-sdk/frontend` (and `cn` into - `@termix/plugin-sdk/ui`) the next time either is touched. Owner: D1, or - host-metrics's Phase B step if it lands first. + `runAdaptivePolling` and `cn` are exported from `@termix/plugin-sdk/ui` + now (B9), and host-metrics uses them there (B16); only proxmox's copies are + left to switch. Owner: D1. - **B5 (proxmox):** `runDueProxmoxAutoSyncs` in `plugins/proxmox/src/backend/routes.ts` still reaches `createCurrentPluginSettingsRepository`/`createCurrentHostRepository` by @@ -102,8 +88,9 @@ build`'s esbuild step has no static-asset-copy pipeline the way core's Vite `logActivity` is a core `@/main-axios` call and the plugin imports nothing from `@/`. Old "tunnel" entries still reopen the tab through the plugin's `activityTypes`. An `app.logActivity` (or equivalent bridge member) would - restore it for this plugin and let docker, file-manager and host-metrics drop - their own `@/main-axios` import for it too. Owner: D1. + restore it for this plugin and let docker and file-manager drop their own + `@/main-axios` import for it too (host-metrics uses the `logActivity` the + ui entry exports). Owner: D1. - **B7 (tunnels):** on the desktop app, tunnel statuses from a connected remote server are no longer merged into the tab: the old code polled the remote tunnel service through `getRemoteTunnelApi()`, and the SDK has no @@ -345,6 +332,46 @@ build`'s esbuild step has no static-asset-copy pipeline the way core's Vite `@termix/plugin-sdk/ui`), workspaces included. `npm run type-check` covers plugins through `tsconfig.plugins*.json` and passes. Owner: D0. +- **B16 (host-metrics):** automations still sets up its headless viewers + through `setViewerRegistry` in `plugins/automations/src/backend/headless-viewer.ts`, + which nothing calls any more, so hosts an automation watches are only + sampled while somebody has them open. Switch it to the + `host-metrics.viewers` service (`register(hostId)` as the automation's + owner, `heartbeat`, `unregister`) and make host-metrics an optional + dependency again. Its triggers already listen to the final topics + (`plugin.host-metrics.snapshot`, `plugin.host-metrics.health-check`, core + `host.status`, `host.login`), but through core's bus by relative import + instead of `ctx.events`. Owner: B17. +- **B16 (host-metrics):** Termix-Mobile read `statsConfig` on each host + (`statusCheckEnabled`, `enabledWidgets`) and the host status from the + metrics API. The host payload now carries `statusCheckEnabled` and + `statusCheckInterval` at the top level, the metrics options under + `pluginSettings["host-metrics"]`, and statuses come from `GET /host/status`. + The mobile app needs the matching change, or the host payload a + `statsConfig` built from those fields for one release. Owner: D1. +- **B16 (host-metrics):** a host's status is only checked once its owner has + asked for statuses (opened the app) since the server started: a user who + only has a host shared with them sees no dot for it until then. This was + already true before B16 (the first user to load the host list started + polling for their own hosts only). Starting checks for the owners of every + host a requester can see would fix it. Owner: D1. +- **B16 (host-metrics):** the host JSON export does not carry host-metrics's + host settings (it did carry `statsConfig`); import reads both the old + `statsConfig` and `pluginSettings["host-metrics"]` through the plugin's + import normalizer. Same gap as the B7 line about exporting plugin host + settings generically. Owner: D1. +- **B16 (host-metrics):** the CPU and network collectors keep their last + sample per host in module-level maps (`widgets/cpu-collector.ts`, + `widgets/network-collector.ts`) to compute rates. The poller clears them on + deactivate and per host, so nothing leaks, but they are still module state + rather than per-activation. Owner: D1. +- **Pre-existing, found in B16:** automations + `tests/backend/tunnels.test.ts` fails "fails the step cleanly while the + tunnels plugin is off": it expects "tunnels plugin is not available" and + gets "tunnels.start is not a function". Same cause as the tunnels line + above: since B12 `ctx.services.get` hands back an empty handle instead of + throwing, so the step has to check `typeof start`. Owner: B17 or D0. + ## Manual checks after 2.9.0 - SSH terminal: password auth, key auth (and an encrypted key's passphrase @@ -362,6 +389,16 @@ build`'s esbuild step has no static-asset-copy pipeline the way core's Vite off records nothing, retention prunes an old recording on schedule, a guacd RDP/VNC/Telnet recording still gets a row and plays back, and deleting a user anonymizes their recordings instead of removing them. +- Host status and metrics: on a 2.8 database, hosts whose status checks were + off stay off and a custom interval survives; the host list dot goes + reachable, then online once the Host Metrics tab or a terminal logs in; + an RDP-only host on a custom port shows online; the metrics tab, the + dashboard host bars, the homepage host status widget, the metrics chart + widget, the terminal toolbar bars and the file manager's disk bar; a + manager card (services, packages) and a health check; TOTP on the metrics + connect; the admin metrics interval, history retention and new-host default; + the temperature unit; disabling host metrics leaves every other plugin's + SSH working, then enabling it again. - Remote desktop: an RDP, a VNC and a Telnet host; a host behind one jump host (tunnels plugin on and off) and behind a chain of two; macOS Screen Sharing over VNC with no username; RDP recording (and none with diff --git a/packages/plugin-sdk/src/backend.ts b/packages/plugin-sdk/src/backend.ts index 53afebb95..a6a5f3d8e 100644 --- a/packages/plugin-sdk/src/backend.ts +++ b/packages/plugin-sdk/src/backend.ts @@ -112,7 +112,14 @@ export interface PluginDatabase { * lives in memory and is saved to its encrypted file only when asked, so a * write without it can be lost on restart. A no-op on Postgres and MySQL. */ - persist: () => Promise; + persist: (options?: { + /** + * Mark the database dirty and let the debounced save pick it up, rather + * than saving now. For frequent, low-value writes such as samples, where + * losing a couple of seconds on a crash is fine. + */ + lazy?: boolean; + }) => Promise; /** * The engine the server runs on. Table objects encode the same on all * three, but MySQL has no RETURNING, so portable code reads a written row @@ -614,6 +621,48 @@ export interface PluginHosts { type: string, hostName: string, ) => Promise; + /** + * Core's reachability status, the one behind the host list's status dot. + * Needs hosts:read. + */ + status: PluginHostStatusApi; +} + +/** online: a login worked. reachable: the port answers. offline: it does not. */ +export type PluginHostStatus = "online" | "reachable" | "offline"; + +export interface PluginHostStatusEntry { + status: PluginHostStatus; + lastChecked: string; + reason?: "host_key_changed"; +} + +/** + * Core checks every host with status checks on by opening a TCP connection to + * it. A plugin that logs in to hosts tells core how that went, which is what + * turns "reachable" into "online". + */ +export interface PluginHostStatusApi { + /** The last known status, or null when core has not checked the host yet. */ + get: (hostId: number) => PluginHostStatusEntry | null; + /** Checks the host now unless core checked it recently. */ + check: (hostId: number) => Promise; + /** Reports a login attempt. Not audited: pollers call it every sample. */ + reportLogin: ( + hostId: number, + outcome: { ok: boolean; hostKeyChanged?: boolean }, + ) => void; + /** + * Tells core which port proves a host of this connection type is up, for + * protocols whose port lives in the plugin's own host settings. Core falls + * back to the host's port. Removed on deactivate. + */ + registerPort: ( + connectionType: string, + resolve: ( + hostId: number, + ) => number | undefined | Promise, + ) => () => void; } /** @@ -782,6 +831,12 @@ export interface PluginSsh { /** The pool key withConnection would use, for clearing it. */ poolKey: (pool: string, host: PluginSshHost) => string; + /** + * Closes this plugin's pooled connections to a host, for when its details + * changed. Only keys this plugin created are touched. + */ + dropPooled: (pool: string, hostId: number) => void; + /** * The host as the acting user may connect to it: RBAC, owner-key * decryption, shared-host overrides and external secret references @@ -1216,6 +1271,28 @@ export interface PluginCapabilities { require: (capability: string) => Promise; } +/** + * Timers owned by the plugin. Every timer is cleared on deactivate, a run that + * throws is logged instead of crashing the server, and a repeating job never + * overlaps itself: a tick that arrives while the last run is still going is + * skipped. No capability needed. + */ +export interface PluginSchedule { + /** Runs `fn` every `intervalMs`. Returns a function that stops it. */ + every: ( + intervalMs: number, + fn: () => void | Promise, + options?: { + /** Delays the first run by up to this much, so many timers spread out. */ + jitterMs?: number; + /** Also run once straight away. */ + runNow?: boolean; + }, + ) => () => void; + /** Runs `fn` once after `delayMs`. Returns a function that cancels it. */ + after: (delayMs: number, fn: () => void | Promise) => () => void; +} + export interface PluginContext { readonly pluginId: string; readonly manifest: PluginManifest; @@ -1247,6 +1324,8 @@ export interface PluginContext { readonly credentials: PluginCredentials; /** Audit lines under the plugin's own action names. */ readonly audit: PluginAudit; + /** Timers that stop on deactivate. */ + readonly schedule: PluginSchedule; /** * 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 2f513dd69..a6aa5a11b 100644 --- a/packages/plugin-sdk/src/frontend.ts +++ b/packages/plugin-sdk/src/frontend.ts @@ -695,6 +695,16 @@ export interface PluginHostBridge { id: string, ) => ComponentType> | undefined; hostProtocols: (host: PluginHostRecord) => string[]; + useHostStatus: (hostId: number | undefined) => HostStatusInfo | null; +} + +/** + * Core's status for a host, the one behind the host list's dot. online: a + * login worked. reachable: the port answers. unknown: not checked yet. + */ +export interface HostStatusInfo { + status: "online" | "reachable" | "offline" | "unknown"; + reason?: "host_key_changed"; } let host: PluginHostBridge | null = null; @@ -765,6 +775,17 @@ export function useHosts(): { hosts: PluginHostRecord[]; loaded: boolean } { return requireHost().useHosts(); } +/** + * Core's status for one host, kept current by the shell's own polling. Null + * outside the app shell (a standalone window) and for hosts with status + * checks off. + */ +export function useHostStatus( + hostId: number | undefined, +): HostStatusInfo | null { + return requireHost().useHostStatus(hostId); +} + export function useCurrentUser(): CurrentUser | null { return requireHost().useCurrentUser(); } diff --git a/packages/plugin-sdk/src/testing.ts b/packages/plugin-sdk/src/testing.ts index 3e0080cbf..a898a0f13 100644 --- a/packages/plugin-sdk/src/testing.ts +++ b/packages/plugin-sdk/src/testing.ts @@ -38,8 +38,17 @@ import type { PluginDatabase } from "./backend.js"; import type { PluginNativeRdpRequest, PluginProtocolTarget, + PluginHostStatusEntry, } from "./backend.js"; +/** A timer registered through ctx.schedule, run by hand with runScheduled. */ +export interface FakeScheduledJob { + kind: "every" | "after"; + ms: number; + fn: () => void | Promise; + stopped: boolean; +} + export interface FakeContextOptions { pluginId?: string; manifest?: Partial; @@ -80,6 +89,8 @@ export interface FakeContextOptions { protocolTargets?: Record; /** What ctx.desktop.available() answers. Defaults to false. */ desktopAvailable?: boolean; + /** What ctx.hosts.status.get and check answer, by host id. */ + hostStatuses?: Record; } export interface FakeAuthRegistrations { @@ -150,6 +161,22 @@ export interface FakePluginContext { interaction: string; request: Record; }>; + /** Every ctx.hosts.status.reportLogin call, in order. */ + statusReports: Array<{ + hostId: number; + ok: boolean; + hostKeyChanged?: boolean; + }>; + /** ctx.hosts.status.registerPort resolvers, by connection type. */ + statusPorts: Map unknown>; + /** Backing store behind ctx.hosts.status.get and check. */ + hostStatuses: Map; + /** Every ctx.ssh.dropPooled call, in order. */ + droppedPools: Array<{ pool: string; hostId: number }>; + /** Every ctx.schedule timer, stopped ones included. */ + scheduled: FakeScheduledJob[]; + /** Runs every live ctx.schedule job once; "after" jobs are then done. */ + runScheduled: () => Promise; /** Changes the acting user, as core's request middleware would. */ setActor: (userId: string | undefined) => void; /** Provided or seeded services, by name or "#". */ @@ -227,6 +254,29 @@ export function createFakeContext( const activities: FakePluginContext["activities"] = []; const trackedSessions: number[] = []; const interactions: FakePluginContext["interactions"] = []; + const statusReports: FakePluginContext["statusReports"] = []; + const statusPorts: FakePluginContext["statusPorts"] = new Map(); + const hostStatuses = new Map( + Object.entries(options.hostStatuses ?? {}).map(([id, entry]) => [ + Number(id), + entry, + ]), + ); + const droppedPools: FakePluginContext["droppedPools"] = []; + const scheduled: FakeScheduledJob[] = []; + const schedule = ( + kind: FakeScheduledJob["kind"], + ms: number, + fn: FakeScheduledJob["fn"], + ) => { + const job: FakeScheduledJob = { kind, ms, fn, stopped: false }; + scheduled.push(job); + const stop = () => { + job.stopped = true; + }; + disposals.push(stop); + return stop; + }; const hostsById = new Map( (options.hosts ?? []).map((h) => [h.id, h]), ); @@ -561,6 +611,28 @@ export function createFakeContext( recordActivity: async (hostId, type, hostName) => { activities.push({ hostId, type, hostName }); }, + status: { + get: (hostId) => hostStatuses.get(hostId) ?? null, + check: async (hostId) => hostStatuses.get(hostId) ?? null, + reportLogin: (hostId, outcome) => { + statusReports.push({ hostId, ...outcome }); + }, + registerPort: (connectionType, resolve) => { + statusPorts.set(connectionType, resolve); + return () => { + statusPorts.delete(connectionType); + }; + }, + }, + }, + + schedule: { + every: (intervalMs, fn, scheduleOptions) => { + const stop = schedule("every", intervalMs, fn); + if (scheduleOptions?.runNow) void fn(); + return stop; + }, + after: (delayMs, fn) => schedule("after", delayMs, fn), }, audit: { @@ -591,6 +663,9 @@ export function createFakeContext( host: { id: 0, ip: "", port: 22, username: "" } as never, dispose: () => {}, }), + dropPooled: (pool, hostId) => { + droppedPools.push({ pool, hostId }); + }, poolKey: (pool, host) => `${pool}:${host.userId}:${host.ip}:${host.port}:${host.username}`, resolveHost: async (hostId, resolveOptions) => @@ -701,6 +776,17 @@ export function createFakeContext( activities, trackedSessions, interactions, + statusReports, + statusPorts, + hostStatuses, + droppedPools, + scheduled, + runScheduled: async () => { + for (const job of scheduled.filter((candidate) => !candidate.stopped)) { + if (job.kind === "after") job.stopped = true; + await job.fn(); + } + }, setActor: (userId) => { actor = userId; }, @@ -739,6 +825,8 @@ export interface MockContextOptions { protocolTargets?: Record; /** What ctx.desktop.available() answers. */ desktopAvailable?: boolean; + /** What ctx.hosts.status answers. See FakeContextOptions. */ + hostStatuses?: Record; } export interface MockPluginContext extends FakePluginContext { @@ -777,6 +865,7 @@ export function createMockCtx( services: options.services, protocolTargets: options.protocolTargets, desktopAvailable: options.desktopAvailable, + hostStatuses: options.hostStatuses, manifest: { capabilities: options.capabilities ?? [], ...options.manifest, @@ -815,9 +904,9 @@ export function createMockCtx( require("db:own"); return guardedDb.refs(); }, - persist: async () => { + persist: async (persistOptions) => { require("db:own"); - return guardedDb.persist(); + return guardedDb.persist(persistOptions); }, dialect: guardedDb.dialect, }, @@ -937,6 +1026,24 @@ export function createMockCtx( require("hosts:read"); return ctx.hosts.recordActivity(hostId, type, hostName); }, + status: { + get: (hostId) => { + require("hosts:read"); + return ctx.hosts.status.get(hostId); + }, + check: async (hostId) => { + require("hosts:read"); + return ctx.hosts.status.check(hostId); + }, + reportLogin: (hostId, outcome) => { + require("hosts:read"); + ctx.hosts.status.reportLogin(hostId, outcome); + }, + registerPort: (connectionType, resolve) => { + require("hosts:read"); + return ctx.hosts.status.registerPort(connectionType, resolve); + }, + }, }, ssh: { diff --git a/packages/plugin-sdk/src/testing/setup.ts b/packages/plugin-sdk/src/testing/setup.ts index 11e3e5fb9..d3607d494 100644 --- a/packages/plugin-sdk/src/testing/setup.ts +++ b/packages/plugin-sdk/src/testing/setup.ts @@ -81,6 +81,7 @@ const offline = () => useSettings: () => ({ values: {}, loaded: true, save: async () => {} }), useHost: () => null, useHosts: () => ({ hosts: [], loaded: true }), + useHostStatus: () => null, useCurrentUser: () => null, useTheme: () => ({ theme: "dark" }), toast: { success: noop, error: noop, info: noop, warning: noop }, diff --git a/plugins/ai/src/backend/tools/executor.ts b/plugins/ai/src/backend/tools/executor.ts index 4ea58d519..7da2324ea 100644 --- a/plugins/ai/src/backend/tools/executor.ts +++ b/plugins/ai/src/backend/tools/executor.ts @@ -4,7 +4,7 @@ import { } from "../../../../../src/backend/database/repositories/factory.js"; import { validateDefinition } from "../../../../automations/src/backend/routes.js"; import { resolveHostById } from "../../../../../src/backend/hosts/host-resolver.js"; -import { execCommand } from "../../../../../src/backend/hosts/metrics-shared/common-utils.js"; +import { execCommand } from "@termix/plugin-sdk/host-commands"; import { withSshConnection } from "../ssh.js"; import { getTool } from "./catalog.js"; import { diff --git a/plugins/ai/tests/backend/tools/proposal-executor.test.ts b/plugins/ai/tests/backend/tools/proposal-executor.test.ts index 1c112c189..1958fe9a1 100644 --- a/plugins/ai/tests/backend/tools/proposal-executor.test.ts +++ b/plugins/ai/tests/backend/tools/proposal-executor.test.ts @@ -27,12 +27,10 @@ vi.mock("../../../../../src/backend/hosts/host-resolver.js", () => ({ })); const execCommand = vi.fn(); -vi.mock( - "../../../../../src/backend/hosts/metrics-shared/common-utils.js", - () => ({ - execCommand: (...args: unknown[]) => execCommand(...args), - }), -); +vi.mock("@termix/plugin-sdk/host-commands", async (importOriginal) => ({ + ...(await importOriginal()), + execCommand: (...args: unknown[]) => execCommand(...args), +})); vi.mock("../../../src/backend/ssh.js", () => ({ withSshConnection: async ( _host: unknown, diff --git a/plugins/automations/src/backend/actions/index.ts b/plugins/automations/src/backend/actions/index.ts index 4b25c8f35..0dd14faa1 100644 --- a/plugins/automations/src/backend/actions/index.ts +++ b/plugins/automations/src/backend/actions/index.ts @@ -2,11 +2,11 @@ import { DEFAULT_STEP_TIMEOUT_MS, type Step, } from "../../../../../src/types/automations.js"; -import { execCommand } from "../../../../../src/backend/hosts/metrics-shared/common-utils.js"; import { + execCommand, execElevated, shellSingleQuote, -} from "../../../../../src/backend/hosts/metrics-shared/exec-elevated.js"; +} from "@termix/plugin-sdk/host-commands"; import { withSshConnection } from "../ssh.js"; import { createCurrentNotificationChannelRepository } from "../../../../../src/backend/database/repositories/factory.js"; import { getSnippet, resolveSnippetCommandFor } from "../snippets.js"; diff --git a/plugins/automations/src/backend/docker-watcher.ts b/plugins/automations/src/backend/docker-watcher.ts index 672d3dbfc..92a26d2fa 100644 --- a/plugins/automations/src/backend/docker-watcher.ts +++ b/plugins/automations/src/backend/docker-watcher.ts @@ -2,7 +2,7 @@ import type { AutomationDefinition } from "../../../../src/types/automations.js" import { createCurrentAutomationRepository } from "../../../../src/backend/database/repositories/factory.js"; import { resolveHostById } from "../../../../src/backend/hosts/host-resolver.js"; import { DataCrypto } from "../../../../src/backend/utils/data-crypto.js"; -import { execCommand } from "../../../../src/backend/hosts/metrics-shared/common-utils.js"; +import { execCommand } from "@termix/plugin-sdk/host-commands"; import { withSshConnection } from "./ssh.js"; import { statsLogger } from "../../../../src/backend/utils/logger.js"; import { onDockerEvent } from "./triggers.js"; diff --git a/plugins/automations/src/backend/triggers.ts b/plugins/automations/src/backend/triggers.ts index 4294758bf..00bed9ee0 100644 --- a/plugins/automations/src/backend/triggers.ts +++ b/plugins/automations/src/backend/triggers.ts @@ -367,18 +367,34 @@ export function subscribeAutomationTriggers(): void { if (unsubscribers.length > 0) return; unsubscribers = [ - pluginEvents.on(TOPICS.hostMetrics, (payload) => + pluginEvents.on("plugin.host-metrics.snapshot", (payload) => onMetrics(payload as MetricEvent), ), pluginEvents.on(TOPICS.hostStatus, (payload) => onStatus(payload as StatusEvent), ), - pluginEvents.on(TOPICS.hostHealthCheck, (payload) => + pluginEvents.on("plugin.host-metrics.health-check", (payload) => onHealthCheck(payload as HealthEvent), ), pluginEvents.on(TOPICS.internalEvent, (payload) => onInternalEvent(payload as InternalEvent), ), + // An SSH login seen by the terminal, which host-metrics used to relay. + pluginEvents.on(TOPICS.hostLogin, (payload) => { + const login = payload as { + hostId: number; + userId: string; + sshUser?: string; + fromIp?: string; + }; + if (!login?.userId) return; + return onInternalEvent({ + event: "user_login", + userId: login.userId, + hostId: login.hostId, + details: { sshUser: login.sshUser, fromIp: login.fromIp }, + }); + }), ]; } diff --git a/plugins/file-manager/src/frontend/FileManager.tsx b/plugins/file-manager/src/frontend/FileManager.tsx index b7bdf9748..a28cc441e 100644 --- a/plugins/file-manager/src/frontend/FileManager.tsx +++ b/plugins/file-manager/src/frontend/FileManager.tsx @@ -44,7 +44,7 @@ import { ConnectionScreen, } from "@termix/plugin-sdk/ui"; import { ConnectionLogPanel } from "@/components/connection/ConnectionLogPanel.tsx"; -import { useConnectionRetry } from "@termix/plugin-sdk/frontend"; +import { invokeAction, useConnectionRetry } from "@termix/plugin-sdk/frontend"; import { copyToClipboard } from "@/lib/clipboard.ts"; import { listSSHFiles, @@ -78,12 +78,11 @@ import { } from "./api/file-manager-data-api"; import { logActivity, - getServerMetricsById, transferToHost, addTransferRecent, type TransferMethodPreference, - type DiskFilesystem, } from "@/main-axios.ts"; +import type { DiskFilesystem, HostDiskInfo } from "./disk-info"; import { getAdaptiveResourceBudget, markAdaptiveResourceUsed, @@ -3262,19 +3261,17 @@ function FileManagerContent({ useEffect(() => { if (currentHost?.id) { loadPinnedFiles(); - getServerMetricsById(currentHost.id) - .then((metrics) => { - if ( - metrics?.disk?.percent != null && - metrics.disk.usedHuman && - metrics.disk.totalHuman - ) { + // Undefined while the host-metrics plugin is off: no disk bar then. + invokeAction("host-metrics.disk", currentHost.id) + .then((result) => { + const disk = result as HostDiskInfo | null | undefined; + if (disk?.percent != null && disk.usedHuman && disk.totalHuman) { setDiskInfo({ - usedHuman: metrics.disk.usedHuman, - totalHuman: metrics.disk.totalHuman, - percent: metrics.disk.percent, - mount: metrics.disk.mount ?? null, - filesystems: metrics.disk.filesystems ?? [], + usedHuman: disk.usedHuman, + totalHuman: disk.totalHuman, + percent: disk.percent, + mount: disk.mount ?? null, + filesystems: disk.filesystems ?? [], }); } }) diff --git a/plugins/file-manager/src/frontend/FileManagerSidebar.tsx b/plugins/file-manager/src/frontend/FileManagerSidebar.tsx index f07e9b486..eb0b0e45a 100644 --- a/plugins/file-manager/src/frontend/FileManagerSidebar.tsx +++ b/plugins/file-manager/src/frontend/FileManagerSidebar.tsx @@ -27,7 +27,7 @@ import { removeFolderShortcut, } from "./api/file-manager-data-api"; import { listSSHFiles } from "./api/ssh-file-operations-api"; -import { type DiskFilesystem } from "@/main-axios.ts"; +import type { DiskFilesystem } from "./disk-info"; import { toast } from "sonner"; import FolderTree from "@/components/folder.tsx"; import { diff --git a/plugins/file-manager/src/frontend/disk-info.ts b/plugins/file-manager/src/frontend/disk-info.ts new file mode 100644 index 000000000..238e82980 --- /dev/null +++ b/plugins/file-manager/src/frontend/disk-info.ts @@ -0,0 +1,22 @@ +/** One mounted filesystem, as the host-metrics plugin reports it. */ +export interface DiskFilesystem { + filesystem: string; + type: string; + mount: string; + percent: number | null; + usedHuman: string | null; + totalHuman: string | null; + availableHuman: string | null; + usedBytes: number | null; + totalBytes: number | null; + availableBytes: number | null; + label?: string; +} + +export interface HostDiskInfo { + percent: number | null; + usedHuman: string | null; + totalHuman: string | null; + mount: string | null; + filesystems?: DiskFilesystem[]; +} diff --git a/plugins/host-metrics/README.md b/plugins/host-metrics/README.md index 02316101e..29847abc2 100644 --- a/plugins/host-metrics/README.md +++ b/plugins/host-metrics/README.md @@ -1,14 +1,18 @@ # Host Metrics -Live CPU, memory, disk, network and process metrics, plus host management (services, packages, firewall, cron, users, SSL, logs, WireGuard, Tailscale). +Live CPU, memory, disk, network and process metrics, plus host management (services, packages, firewall, cron, users, SSL, logs, WireGuard). Other plugins add manager cards through the `host-metrics.managers` slot (Tailscale does). + +The host list's online and offline dot is core's own status check, not this plugin. A working metrics login is what turns a reachable host online. Bundled with Termix and enabled by default. ## Layout manifest.json id, capabilities and what this plugin contributes - src/backend/ activate(ctx) / deactivate(), plus the routes + src/backend/ activate(ctx) / deactivate(), the poller and the routes src/frontend/ the UI it registers + src/shared/ types both sides use + migrations/ adopts core's four metrics tables (sqlite, postgres, mysql) locales/en.json its strings (English only; the rest are translated) tests/backend/ vitest, node tests/frontend/ vitest, jsdom diff --git a/plugins/host-metrics/locales/en.json b/plugins/host-metrics/locales/en.json index 254edf3cc..59a616e28 100644 --- a/plugins/host-metrics/locales/en.json +++ b/plugins/host-metrics/locales/en.json @@ -182,7 +182,8 @@ "wgLastHandshake": "Last handshake", "wgHandshakeNever": "Never", "wgTransfer": "Transfer" - } + }, + "snippetsUnavailable": "The Snippets plugin is off, so quick actions cannot run." }, "metricsHistory": { "title": "Metrics History", @@ -223,7 +224,9 @@ "noQuickActions": "No quick actions yet.", "buttonLabel": "Button label", "selectSnippetPlaceholder": "Select snippet...", - "addActionBtn": "Add Action" + "addActionBtn": "Add Action", + "hostNotFound": "Host not found", + "loadingHost": "Loading host..." }, "onboarding": { "feature_metrics": "Metrics and alerts", @@ -238,9 +241,91 @@ "noData": "No data available", "showCurrentValue": "Show Current Value", "chartMetric": "Metric", - "metricRange": "Range" + "metricRange": "Range", + "host": "Host", + "loading": "Loading...", + "metricCpu": "CPU", + "metricDisk": "Disk", + "metricHostname": "Hostname", + "metricKernel": "Kernel", + "metricMemory": "Memory", + "metricNetwork": "Network", + "metricOs": "OS", + "metricProcesses": "Processes", + "metricProcessesRunning": "running", + "metricProcessesTotal": "total", + "metricUptime": "Uptime", + "metricsNotAvailable": "Metrics not available", + "noHostSelected": "No host selected", + "selectHost": "Select a host..." }, "terminalStatus": { "unavailable": "Live stats unavailable" + }, + "dashboard": { + "cpu": "CPU", + "ram": "RAM" + }, + "dashboardTab": { + "disk": "Disk" + }, + "newUi": { + "sidebar": { + "snippets": { + "cancel": "Cancel", + "confirmRunButton": "Run", + "confirmRunMessage": "Run \"{{name}}\"?" + } + } + }, + "terminal": { + "connected": "Connected" + }, + "permissions": { + "use": { + "title": "Use Host Metrics", + "description": "View host metrics and use the host managers (services, packages, firewall and more) on hosts you can reach." + } + }, + "settings": { + "admin": { + "metricsInterval": { + "label": "Metrics interval (seconds)", + "description": "How often a host being viewed is sampled, unless the host sets its own interval." + }, + "historyRetentionDays": { + "label": "History retention (days)", + "description": "How long metrics samples are kept for the history charts." + }, + "enabledForNewHosts": { + "label": "Metrics on for new hosts", + "description": "Whether new hosts start with metrics collection turned on." + } + }, + "user": { + "temperatureUnit": { + "label": "Temperature unit", + "description": "How temperatures are shown in Host Metrics.", + "celsius": "Celsius", + "fahrenheit": "Fahrenheit" + } + }, + "host": { + "metricsEnabled": { + "label": "Collect metrics" + }, + "metricsInterval": { + "label": "Metrics interval (seconds)" + }, + "enabledWidgets": { + "label": "Enabled widgets" + }, + "excludedMounts": { + "label": "Excluded mounts" + }, + "monitoredMounts": { + "label": "Monitored paths" + } + } } } diff --git a/plugins/host-metrics/locales/translated/af_ZA.json b/plugins/host-metrics/locales/translated/af_ZA.json index 2223e3aa9..48a45e1b7 100644 --- a/plugins/host-metrics/locales/translated/af_ZA.json +++ b/plugins/host-metrics/locales/translated/af_ZA.json @@ -240,7 +240,9 @@ "noQuickActions": "Geen vinnige aksies nog nie.", "buttonLabel": "Knoppie-etiket", "selectSnippetPlaceholder": "Kies uittreksel...", - "addActionBtn": "Voeg Aksie by" + "addActionBtn": "Voeg Aksie by", + "hostNotFound": "Gasheer nie gevind nie", + "loadingHost": "Laai gasheer..." }, "onboarding": { "feature_metrics": "Metrieke en waarskuwings", @@ -255,6 +257,41 @@ "noData": "Geen data beskikbaar nie", "showCurrentValue": "Wys huidige waarde", "chartMetric": "Metrieke", - "metricRange": "Reikwydte" + "metricRange": "Reikwydte", + "host": "Gasheer", + "loading": "Laai tans...", + "metricCpu": "SVE", + "metricDisk": "Skyf", + "metricHostname": "Gasheernaam", + "metricKernel": "Kern", + "metricMemory": "Geheue", + "metricNetwork": "Netwerk", + "metricOs": "OS", + "metricProcesses": "Prosesse", + "metricProcessesRunning": "hardloop", + "metricProcessesTotal": "totaal", + "metricUptime": "Optyd", + "metricsNotAvailable": "Metrieke nie beskikbaar nie", + "noHostSelected": "Geen gasheer gekies nie", + "selectHost": "Kies 'n gasheer..." + }, + "dashboard": { + "cpu": "SVE", + "ram": "RAM" + }, + "dashboardTab": { + "disk": "Skyf" + }, + "newUi": { + "sidebar": { + "snippets": { + "cancel": "Kanselleer", + "confirmRunButton": "Hardloop", + "confirmRunMessage": "Begin \"{{name}}\"?" + } + } + }, + "terminal": { + "connected": "Verbonde" } } diff --git a/plugins/host-metrics/locales/translated/ar_SA.json b/plugins/host-metrics/locales/translated/ar_SA.json index 9d34ada56..d8c9f111d 100644 --- a/plugins/host-metrics/locales/translated/ar_SA.json +++ b/plugins/host-metrics/locales/translated/ar_SA.json @@ -240,7 +240,9 @@ "noQuickActions": "لا توجد إجراءات سريعة بعد.", "buttonLabel": "تسمية الزر", "selectSnippetPlaceholder": "اختر مقتطفًا...", - "addActionBtn": "إضافة إجراء" + "addActionBtn": "إضافة إجراء", + "hostNotFound": "المضيف غير موجود", + "loadingHost": "جارٍ تحميل المضيف..." }, "onboarding": { "feature_metrics": "المقاييس والتنبيهات", @@ -255,6 +257,41 @@ "noData": "لا توجد بيانات", "showCurrentValue": "إظهار القيمة الحالية", "chartMetric": "المقياس", - "metricRange": "النطاق" + "metricRange": "النطاق", + "host": "المضيف", + "loading": "جار التحميل...", + "metricCpu": "CPU", + "metricDisk": "القرص", + "metricHostname": "اسم المضيف", + "metricKernel": "النواة", + "metricMemory": "الذاكرة", + "metricNetwork": "الشبكة", + "metricOs": "نظام التشغيل", + "metricProcesses": "العمليات", + "metricProcessesRunning": "قيد التشغيل", + "metricProcessesTotal": "إجمالي", + "metricUptime": "وقت التشغيل", + "metricsNotAvailable": "المقاييس غير متوفرة", + "noHostSelected": "لم يتم تحديد أي مضيف", + "selectHost": "اختيار مضيف..." + }, + "dashboard": { + "cpu": "المعالج", + "ram": "الذاكرة" + }, + "dashboardTab": { + "disk": "القرص" + }, + "newUi": { + "sidebar": { + "snippets": { + "cancel": "إلغاء", + "confirmRunButton": "تشغيل", + "confirmRunMessage": "تشغيل \"{{name}}\"؟" + } + } + }, + "terminal": { + "connected": "متصل" } } diff --git a/plugins/host-metrics/locales/translated/bg_BG.json b/plugins/host-metrics/locales/translated/bg_BG.json index 9c021a99b..5322ceace 100644 --- a/plugins/host-metrics/locales/translated/bg_BG.json +++ b/plugins/host-metrics/locales/translated/bg_BG.json @@ -240,7 +240,9 @@ "noQuickActions": "Все още няма бързи действия.", "buttonLabel": "Етикет на бутона", "selectSnippetPlaceholder": "Изберете фрагмент...", - "addActionBtn": "Добавяне на действие" + "addActionBtn": "Добавяне на действие", + "hostNotFound": "Хостът не е намерен", + "loadingHost": "Зареждане на хоста..." }, "onboarding": { "feature_metrics": "Показатели и сигнали", @@ -255,6 +257,41 @@ "noData": "Няма налични данни", "showCurrentValue": "Показване на текущата стойност", "chartMetric": "Метричен", - "metricRange": "Диапазон" + "metricRange": "Диапазон", + "host": "Домакин", + "loading": "Зареждане...", + "metricCpu": "Процесор", + "metricDisk": "Диск", + "metricHostname": "Име на хост", + "metricKernel": "Ядро", + "metricMemory": "Памет", + "metricNetwork": "Мрежа", + "metricOs": "ОС", + "metricProcesses": "Процеси", + "metricProcessesRunning": "бягане", + "metricProcessesTotal": "общо", + "metricUptime": "Време на работа", + "metricsNotAvailable": "Показателите не са налични", + "noHostSelected": "Няма избран хост", + "selectHost": "Изберете хост..." + }, + "dashboard": { + "cpu": "Процесор", + "ram": "RAM памет" + }, + "dashboardTab": { + "disk": "Диск" + }, + "newUi": { + "sidebar": { + "snippets": { + "cancel": "Отказ", + "confirmRunButton": "Бягане", + "confirmRunMessage": "Изпълни „{{name}}“?" + } + } + }, + "terminal": { + "connected": "Свързан" } } diff --git a/plugins/host-metrics/locales/translated/bn_BD.json b/plugins/host-metrics/locales/translated/bn_BD.json index 28710069c..8f7447a44 100644 --- a/plugins/host-metrics/locales/translated/bn_BD.json +++ b/plugins/host-metrics/locales/translated/bn_BD.json @@ -240,7 +240,9 @@ "noQuickActions": "এখনো কোনো দ্রুত পদক্ষেপ নেওয়া হয়নি।", "buttonLabel": "বোতাম লেবেল", "selectSnippetPlaceholder": "স্নিপেট নির্বাচন করুন...", - "addActionBtn": "অ্যাকশন যোগ করুন" + "addActionBtn": "অ্যাকশন যোগ করুন", + "hostNotFound": "হোস্ট খুঁজে পাওয়া যায়নি", + "loadingHost": "হোস্ট লোড হচ্ছে..." }, "onboarding": { "feature_metrics": "মেট্রিক্স এবং সতর্কতা", @@ -255,6 +257,41 @@ "noData": "কোন ডেটা উপলব্ধ নেই", "showCurrentValue": "বর্তমান মান দেখান", "chartMetric": "মেট্রিক", - "metricRange": "পরিসর" + "metricRange": "পরিসর", + "host": "হোস্ট", + "loading": "লোড হচ্ছে...", + "metricCpu": "সিপিইউ", + "metricDisk": "ডিস্ক", + "metricHostname": "হোস্টনাম", + "metricKernel": "কার্নেল", + "metricMemory": "স্মৃতি", + "metricNetwork": "নেটওয়ার্ক", + "metricOs": "ওএস", + "metricProcesses": "প্রক্রিয়া", + "metricProcessesRunning": "দৌড়ানো", + "metricProcessesTotal": "মোট", + "metricUptime": "আপটাইম", + "metricsNotAvailable": "মেট্রিক্স উপলব্ধ নেই", + "noHostSelected": "কোন হোস্ট নির্বাচন করা হয়নি", + "selectHost": "একজন হোস্ট নির্বাচন করুন..." + }, + "dashboard": { + "cpu": "সিপিইউ", + "ram": "রাম" + }, + "dashboardTab": { + "disk": "ডিস্ক" + }, + "newUi": { + "sidebar": { + "snippets": { + "cancel": "বাতিল করুন", + "confirmRunButton": "দৌড়", + "confirmRunMessage": "\"{{name}} \" চালান?" + } + } + }, + "terminal": { + "connected": "সংযুক্ত" } } diff --git a/plugins/host-metrics/locales/translated/ca_ES.json b/plugins/host-metrics/locales/translated/ca_ES.json index 2b13bc95d..284c80128 100644 --- a/plugins/host-metrics/locales/translated/ca_ES.json +++ b/plugins/host-metrics/locales/translated/ca_ES.json @@ -240,7 +240,9 @@ "noQuickActions": "Encara no hi ha accions ràpides.", "buttonLabel": "Etiqueta del botó", "selectSnippetPlaceholder": "Selecciona un fragment...", - "addActionBtn": "Afegeix una acció" + "addActionBtn": "Afegeix una acció", + "hostNotFound": "No s'ha trobat l'amfitrió", + "loadingHost": "S'està carregant l'amfitrió..." }, "onboarding": { "feature_metrics": "Mètriques i alertes", @@ -255,6 +257,41 @@ "noData": "No hi ha dades disponibles", "showCurrentValue": "Mostra el valor actual", "chartMetric": "Mètrica", - "metricRange": "Rang" + "metricRange": "Rang", + "host": "Amfitrió", + "loading": "S'està carregant...", + "metricCpu": "CPU", + "metricDisk": "Disc", + "metricHostname": "Nom d'amfitrió", + "metricKernel": "Nucli", + "metricMemory": "Memòria", + "metricNetwork": "Xarxa", + "metricOs": "Sistema operatiu", + "metricProcesses": "Processos", + "metricProcessesRunning": "corrent", + "metricProcessesTotal": "total", + "metricUptime": "Temps de funcionament", + "metricsNotAvailable": "Mètriques no disponibles", + "noHostSelected": "No s'ha seleccionat cap amfitrió", + "selectHost": "Seleccioneu un amfitrió..." + }, + "dashboard": { + "cpu": "CPU", + "ram": "RAM" + }, + "dashboardTab": { + "disk": "Disc" + }, + "newUi": { + "sidebar": { + "snippets": { + "cancel": "Cancel·la", + "confirmRunButton": "Corre", + "confirmRunMessage": "Executar \"{{name}}\"?" + } + } + }, + "terminal": { + "connected": "Connectat" } } diff --git a/plugins/host-metrics/locales/translated/cs_CZ.json b/plugins/host-metrics/locales/translated/cs_CZ.json index ec2ef8ae6..69ab3e17b 100644 --- a/plugins/host-metrics/locales/translated/cs_CZ.json +++ b/plugins/host-metrics/locales/translated/cs_CZ.json @@ -240,7 +240,9 @@ "noQuickActions": "Zatím žádné rychlé akce.", "buttonLabel": "Štítek tlačítka", "selectSnippetPlaceholder": "Vybrat úryvek...", - "addActionBtn": "Přidat akci" + "addActionBtn": "Přidat akci", + "hostNotFound": "Hostitel nenalezen", + "loadingHost": "Načítání hostitele..." }, "onboarding": { "feature_metrics": "Metriky a upozornění", @@ -255,6 +257,41 @@ "noData": "Nejsou k dispozici žádná data", "showCurrentValue": "Zobrazit aktuální hodnotu", "chartMetric": "Metrický", - "metricRange": "Rozsah" + "metricRange": "Rozsah", + "host": "Hostitel", + "loading": "Načítání...", + "metricCpu": "Procesor", + "metricDisk": "Disk", + "metricHostname": "Název hostitele", + "metricKernel": "Jádro", + "metricMemory": "Paměť", + "metricNetwork": "Síť", + "metricOs": "OS", + "metricProcesses": "Procesy", + "metricProcessesRunning": "běh", + "metricProcessesTotal": "celkový", + "metricUptime": "Doba provozuschopnosti", + "metricsNotAvailable": "Metriky nejsou k dispozici", + "noHostSelected": "Nebyl vybrán žádný hostitel", + "selectHost": "Vyberte hostitele..." + }, + "dashboard": { + "cpu": "Procesor", + "ram": "BERAN" + }, + "dashboardTab": { + "disk": "Disk" + }, + "newUi": { + "sidebar": { + "snippets": { + "cancel": "Zrušit", + "confirmRunButton": "Běh", + "confirmRunMessage": "Spustit „{{name}}“?" + } + } + }, + "terminal": { + "connected": "Připojeno" } } diff --git a/plugins/host-metrics/locales/translated/da_DK.json b/plugins/host-metrics/locales/translated/da_DK.json index 0ea7ea656..bb4dc6cb3 100644 --- a/plugins/host-metrics/locales/translated/da_DK.json +++ b/plugins/host-metrics/locales/translated/da_DK.json @@ -240,7 +240,9 @@ "noQuickActions": "Ingen hurtige handlinger endnu.", "buttonLabel": "Knapmærke", "selectSnippetPlaceholder": "Vælg uddrag...", - "addActionBtn": "Tilføj handling" + "addActionBtn": "Tilføj handling", + "hostNotFound": "Værten blev ikke fundet", + "loadingHost": "Indlæser vært..." }, "onboarding": { "feature_metrics": "Målinger og advarsler", @@ -255,6 +257,41 @@ "noData": "Ingen data tilgængelige", "showCurrentValue": "Vis aktuel værdi", "chartMetric": "Metrisk", - "metricRange": "Rækkevidde" + "metricRange": "Rækkevidde", + "host": "Vært", + "loading": "Indlæser...", + "metricCpu": "CPU", + "metricDisk": "Disk", + "metricHostname": "Værtsnavn", + "metricKernel": "Kernen", + "metricMemory": "Hukommelse", + "metricNetwork": "Netværk", + "metricOs": "OS", + "metricProcesses": "Processer", + "metricProcessesRunning": "løb", + "metricProcessesTotal": "total", + "metricUptime": "Oppetid", + "metricsNotAvailable": "Målinger er ikke tilgængelige", + "noHostSelected": "Ingen vært valgt", + "selectHost": "Vælg en vært..." + }, + "dashboard": { + "cpu": "CPU", + "ram": "VÆDDER" + }, + "dashboardTab": { + "disk": "Disk" + }, + "newUi": { + "sidebar": { + "snippets": { + "cancel": "Ophæve", + "confirmRunButton": "Løbe", + "confirmRunMessage": "Kør \"{{name}}\"?" + } + } + }, + "terminal": { + "connected": "Forbundet" } } diff --git a/plugins/host-metrics/locales/translated/de_DE.json b/plugins/host-metrics/locales/translated/de_DE.json index ecee020ae..a656a90a4 100644 --- a/plugins/host-metrics/locales/translated/de_DE.json +++ b/plugins/host-metrics/locales/translated/de_DE.json @@ -240,7 +240,9 @@ "noQuickActions": "Noch keine Schnellaktionen.", "buttonLabel": "Schaltflächenbeschriftung", "selectSnippetPlaceholder": "Snippet auswählen...", - "addActionBtn": "Aktion hinzufügen" + "addActionBtn": "Aktion hinzufügen", + "hostNotFound": "Host nicht gefunden", + "loadingHost": "Host wird geladen..." }, "onboarding": { "feature_metrics": "Kennzahlen und Warnmeldungen", @@ -255,6 +257,41 @@ "noData": "Keine Daten verfügbar", "showCurrentValue": "Aktuellen Wert anzeigen", "chartMetric": "Metrik", - "metricRange": "Bereich" + "metricRange": "Bereich", + "host": "Host", + "loading": "Wird geladen...", + "metricCpu": "CPU", + "metricDisk": "Festplatte", + "metricHostname": "Hostname", + "metricKernel": "Kernel", + "metricMemory": "Arbeitsspeicher", + "metricNetwork": "Netzwerk", + "metricOs": "Betriebssystem", + "metricProcesses": "Prozesse", + "metricProcessesRunning": "laufend", + "metricProcessesTotal": "gesamt", + "metricUptime": "Betriebszeit", + "metricsNotAvailable": "Metriken nicht verfügbar", + "noHostSelected": "Kein Host ausgewählt", + "selectHost": "Host auswählen..." + }, + "dashboard": { + "cpu": "CPU", + "ram": "RAM" + }, + "dashboardTab": { + "disk": "Datenträger" + }, + "newUi": { + "sidebar": { + "snippets": { + "cancel": "Abbrechen", + "confirmRunButton": "Ausführen", + "confirmRunMessage": "„{{name}}“ ausführen?" + } + } + }, + "terminal": { + "connected": "Verbunden" } } diff --git a/plugins/host-metrics/locales/translated/el_GR.json b/plugins/host-metrics/locales/translated/el_GR.json index 69b37e229..c561a0926 100644 --- a/plugins/host-metrics/locales/translated/el_GR.json +++ b/plugins/host-metrics/locales/translated/el_GR.json @@ -240,7 +240,9 @@ "noQuickActions": "Δεν υπάρχουν ακόμη γρήγορες ενέργειες.", "buttonLabel": "Ετικέτα κουμπιού", "selectSnippetPlaceholder": "Επιλογή αποσπάσματος...", - "addActionBtn": "Προσθήκη ενέργειας" + "addActionBtn": "Προσθήκη ενέργειας", + "hostNotFound": "Δεν βρέθηκε κεντρικός υπολογιστής", + "loadingHost": "Φόρτωση κεντρικού υπολογιστή..." }, "onboarding": { "feature_metrics": "Μετρήσεις και ειδοποιήσεις", @@ -255,6 +257,41 @@ "noData": "Δεν υπάρχουν διαθέσιμα δεδομένα", "showCurrentValue": "Εμφάνιση τρέχουσας τιμής", "chartMetric": "Μετρικός", - "metricRange": "Σειρά" + "metricRange": "Σειρά", + "host": "Πλήθος", + "loading": "Φόρτωση...", + "metricCpu": "ΚΜΕ", + "metricDisk": "Δίσκος", + "metricHostname": "Όνομα κεντρικού υπολογιστή", + "metricKernel": "Πυρήνας", + "metricMemory": "Μνήμη", + "metricNetwork": "Δίκτυο", + "metricOs": "Λειτουργικό σύστημα", + "metricProcesses": "Διαδικασίες", + "metricProcessesRunning": "τρέξιμο", + "metricProcessesTotal": "σύνολο", + "metricUptime": "Χρόνος λειτουργίας", + "metricsNotAvailable": "Οι μετρήσεις δεν είναι διαθέσιμες", + "noHostSelected": "Δεν έχει επιλεγεί κεντρικός υπολογιστής", + "selectHost": "Επιλέξτε έναν οικοδεσπότη..." + }, + "dashboard": { + "cpu": "ΚΜΕ", + "ram": "ΕΜΒΟΛΟ" + }, + "dashboardTab": { + "disk": "Δίσκος" + }, + "newUi": { + "sidebar": { + "snippets": { + "cancel": "Ματαίωση", + "confirmRunButton": "Τρέξιμο", + "confirmRunMessage": "Εκτέλεση \"{{name}}\";" + } + } + }, + "terminal": { + "connected": "Συνδεδεμένος" } } diff --git a/plugins/host-metrics/locales/translated/es_ES.json b/plugins/host-metrics/locales/translated/es_ES.json index fdd29a175..1bc356fa9 100644 --- a/plugins/host-metrics/locales/translated/es_ES.json +++ b/plugins/host-metrics/locales/translated/es_ES.json @@ -240,7 +240,9 @@ "noQuickActions": "Aún no hay acciones rápidas.", "buttonLabel": "Etiqueta del botón", "selectSnippetPlaceholder": "Seleccionar fragmento...", - "addActionBtn": "Añadir acción" + "addActionBtn": "Añadir acción", + "hostNotFound": "Host no encontrado", + "loadingHost": "Cargando host..." }, "onboarding": { "feature_metrics": "Métricas y alertas", @@ -255,6 +257,41 @@ "noData": "Sin datos disponibles", "showCurrentValue": "Mostrar valor actual", "chartMetric": "Métrica", - "metricRange": "Rango" + "metricRange": "Rango", + "host": "Host", + "loading": "Cargando...", + "metricCpu": "CPU", + "metricDisk": "Disco", + "metricHostname": "Nombre de host", + "metricKernel": "Kernel", + "metricMemory": "Memoria", + "metricNetwork": "Red", + "metricOs": "SO", + "metricProcesses": "Procesos", + "metricProcessesRunning": "en ejecución", + "metricProcessesTotal": "total", + "metricUptime": "Tiempo de actividad", + "metricsNotAvailable": "Métricas no disponibles", + "noHostSelected": "Ningún host seleccionado", + "selectHost": "Seleccionar un host..." + }, + "dashboard": { + "cpu": "CPU", + "ram": "RAM" + }, + "dashboardTab": { + "disk": "Disco" + }, + "newUi": { + "sidebar": { + "snippets": { + "cancel": "Cancelar", + "confirmRunButton": "Ejecutar", + "confirmRunMessage": "¿Ejecutar \"{{name}}\"?" + } + } + }, + "terminal": { + "connected": "Conectado" } } diff --git a/plugins/host-metrics/locales/translated/fi_FI.json b/plugins/host-metrics/locales/translated/fi_FI.json index bc1236490..92295782a 100644 --- a/plugins/host-metrics/locales/translated/fi_FI.json +++ b/plugins/host-metrics/locales/translated/fi_FI.json @@ -240,7 +240,9 @@ "noQuickActions": "Ei vielä nopeita toimia.", "buttonLabel": "Painikkeen nimi", "selectSnippetPlaceholder": "Valitse katkelma...", - "addActionBtn": "Lisää toiminto" + "addActionBtn": "Lisää toiminto", + "hostNotFound": "Isäntää ei löydy", + "loadingHost": "Ladataan isäntää..." }, "onboarding": { "feature_metrics": "Mittarit ja hälytykset", @@ -255,6 +257,41 @@ "noData": "Ei tietoja saatavilla", "showCurrentValue": "Näytä nykyinen arvo", "chartMetric": "Metrinen", - "metricRange": "Alue" + "metricRange": "Alue", + "host": "Isäntä", + "loading": "Ladataan...", + "metricCpu": "Suoritin", + "metricDisk": "Levy", + "metricHostname": "Isäntänimi", + "metricKernel": "Ydin", + "metricMemory": "Muisti", + "metricNetwork": "Verkko", + "metricOs": "Käyttöjärjestelmä", + "metricProcesses": "Prosessit", + "metricProcessesRunning": "juokseminen", + "metricProcessesTotal": "kokonais-", + "metricUptime": "Käyttöaika", + "metricsNotAvailable": "Mittareita ei ole saatavilla", + "noHostSelected": "Ei valittua isäntää", + "selectHost": "Valitse isäntä..." + }, + "dashboard": { + "cpu": "Suoritin", + "ram": "RAM-muistia" + }, + "dashboardTab": { + "disk": "Levy" + }, + "newUi": { + "sidebar": { + "snippets": { + "cancel": "Peruuttaa", + "confirmRunButton": "Juokse", + "confirmRunMessage": "Suorittaa \"{{name}}\"?" + } + } + }, + "terminal": { + "connected": "Yhdistetty" } } diff --git a/plugins/host-metrics/locales/translated/fr_FR.json b/plugins/host-metrics/locales/translated/fr_FR.json index d46398384..a2247ec6c 100644 --- a/plugins/host-metrics/locales/translated/fr_FR.json +++ b/plugins/host-metrics/locales/translated/fr_FR.json @@ -240,7 +240,9 @@ "noQuickActions": "Aucune action rapide pour le moment.", "buttonLabel": "Étiquette du bouton", "selectSnippetPlaceholder": "Sélectionner un snippet...", - "addActionBtn": "Ajouter une action" + "addActionBtn": "Ajouter une action", + "hostNotFound": "Hôte introuvable", + "loadingHost": "Chargement de l'hôte..." }, "onboarding": { "feature_metrics": "Métriques et alertes", @@ -255,6 +257,41 @@ "noData": "Aucune donnée disponible", "showCurrentValue": "Afficher la valeur actuelle", "chartMetric": "Métrique", - "metricRange": "Plage" + "metricRange": "Plage", + "host": "Hôte", + "loading": "Chargement…", + "metricCpu": "CPU", + "metricDisk": "Disque", + "metricHostname": "Nom d’hôte", + "metricKernel": "Noyau", + "metricMemory": "Mémoire", + "metricNetwork": "Réseau", + "metricOs": "OS", + "metricProcesses": "Processus", + "metricProcessesRunning": "en cours", + "metricProcessesTotal": "total", + "metricUptime": "Uptime", + "metricsNotAvailable": "Métriques non disponibles", + "noHostSelected": "Aucun hôte sélectionné", + "selectHost": "Sélectionner un hôte…" + }, + "dashboard": { + "cpu": "CPU", + "ram": "RAM" + }, + "dashboardTab": { + "disk": "Disque" + }, + "newUi": { + "sidebar": { + "snippets": { + "cancel": "Annuler", + "confirmRunButton": "Exécuter", + "confirmRunMessage": "Exécuter « {{name}} » ?" + } + } + }, + "terminal": { + "connected": "Connecté" } } diff --git a/plugins/host-metrics/locales/translated/he_IL.json b/plugins/host-metrics/locales/translated/he_IL.json index 2570887e0..a6af986a9 100644 --- a/plugins/host-metrics/locales/translated/he_IL.json +++ b/plugins/host-metrics/locales/translated/he_IL.json @@ -240,7 +240,9 @@ "noQuickActions": "אין עדיין פעולות מהירות.", "buttonLabel": "תווית כפתור", "selectSnippetPlaceholder": "בחר קטע...", - "addActionBtn": "הוסף פעולה" + "addActionBtn": "הוסף פעולה", + "hostNotFound": "המארח לא נמצא", + "loadingHost": "טוען מארח..." }, "onboarding": { "feature_metrics": "מדדים והתראות", @@ -255,6 +257,41 @@ "noData": "אין נתונים זמינים", "showCurrentValue": "הצג ערך נוכחי", "chartMetric": "מֶטרִי", - "metricRange": "לָנוּעַ" + "metricRange": "לָנוּעַ", + "host": "מְאָרֵחַ", + "loading": "טְעִינָה...", + "metricCpu": "מעבד", + "metricDisk": "דִיסק", + "metricHostname": "שם מארח", + "metricKernel": "גַרעִין", + "metricMemory": "זֵכֶר", + "metricNetwork": "רֶשֶׁת", + "metricOs": "מערכת הפעלה", + "metricProcesses": "תהליכים", + "metricProcessesRunning": "רִיצָה", + "metricProcessesTotal": "סַך הַכֹּל", + "metricUptime": "זמן פעולה", + "metricsNotAvailable": "מדדים אינם זמינים", + "noHostSelected": "לא נבחר מארח", + "selectHost": "בחר מארח..." + }, + "dashboard": { + "cpu": "מעבד", + "ram": "אַיִל" + }, + "dashboardTab": { + "disk": "דִיסק" + }, + "newUi": { + "sidebar": { + "snippets": { + "cancel": "לְבַטֵל", + "confirmRunButton": "לָרוּץ", + "confirmRunMessage": "להפעיל את \"{{name}}\"?" + } + } + }, + "terminal": { + "connected": "מְחוּבָּר" } } diff --git a/plugins/host-metrics/locales/translated/hi_IN.json b/plugins/host-metrics/locales/translated/hi_IN.json index f1a9d420e..005133f7f 100644 --- a/plugins/host-metrics/locales/translated/hi_IN.json +++ b/plugins/host-metrics/locales/translated/hi_IN.json @@ -240,7 +240,9 @@ "noQuickActions": "अभी तक कोई त्वरित कार्रवाई नहीं।", "buttonLabel": "बटन लेबल", "selectSnippetPlaceholder": "स्निपेट चुनें...", - "addActionBtn": "कार्रवाई जोड़ें" + "addActionBtn": "कार्रवाई जोड़ें", + "hostNotFound": "होस्ट नहीं मिला", + "loadingHost": "होस्ट लोड हो रहा है..." }, "onboarding": { "feature_metrics": "मैट्रिक्स और अलर्ट", @@ -255,6 +257,41 @@ "noData": "कोई डेटा उपलब्ध नहीं", "showCurrentValue": "वर्तमान मान दिखाएं", "chartMetric": "मीट्रिक", - "metricRange": "सीमा" + "metricRange": "सीमा", + "host": "होस्ट", + "loading": "लोड हो रहा है...", + "metricCpu": "CPU", + "metricDisk": "डिस्क", + "metricHostname": "होस्टनेम", + "metricKernel": "कर्नेल", + "metricMemory": "मेमोरी", + "metricNetwork": "नेटवर्क", + "metricOs": "ओएस", + "metricProcesses": "प्रोसेस", + "metricProcessesRunning": "चालू", + "metricProcessesTotal": "कुल", + "metricUptime": "अपटाइम", + "metricsNotAvailable": "मेट्रिक्स उपलब्ध नहीं", + "noHostSelected": "कोई होस्ट चयनित नहीं", + "selectHost": "होस्ट चुनें..." + }, + "dashboard": { + "cpu": "CPU", + "ram": "RAM" + }, + "dashboardTab": { + "disk": "डिस्क" + }, + "newUi": { + "sidebar": { + "snippets": { + "cancel": "रद्द करें", + "confirmRunButton": "चलाएँ", + "confirmRunMessage": "\"{{name}}\" चलाएँ?" + } + } + }, + "terminal": { + "connected": "कनेक्टेड" } } diff --git a/plugins/host-metrics/locales/translated/hu_HU.json b/plugins/host-metrics/locales/translated/hu_HU.json index dba20021a..0deb6c830 100644 --- a/plugins/host-metrics/locales/translated/hu_HU.json +++ b/plugins/host-metrics/locales/translated/hu_HU.json @@ -240,7 +240,9 @@ "noQuickActions": "Még nincsenek gyors intézkedések.", "buttonLabel": "Gomb felirata", "selectSnippetPlaceholder": "Válasszon részletet...", - "addActionBtn": "Művelet hozzáadása" + "addActionBtn": "Művelet hozzáadása", + "hostNotFound": "Nem található gazdagép", + "loadingHost": "Gazdagép betöltése..." }, "onboarding": { "feature_metrics": "Metrikák és riasztások", @@ -255,6 +257,41 @@ "noData": "Nincsenek elérhető adatok", "showCurrentValue": "Aktuális érték megjelenítése", "chartMetric": "Metrika", - "metricRange": "Hatótávolság" + "metricRange": "Hatótávolság", + "host": "Házigazda", + "loading": "Terhelés...", + "metricCpu": "CPU", + "metricDisk": "Korong", + "metricHostname": "Gazdagépnév", + "metricKernel": "Kernel", + "metricMemory": "Memória", + "metricNetwork": "Hálózat", + "metricOs": "operációs rendszer", + "metricProcesses": "Folyamatok", + "metricProcessesRunning": "futás", + "metricProcessesTotal": "teljes", + "metricUptime": "Üzemidő", + "metricsNotAvailable": "Mutatók nem állnak rendelkezésre", + "noHostSelected": "Nincs kiválasztva gazdagép", + "selectHost": "Válasszon egy hosztot..." + }, + "dashboard": { + "cpu": "CPU", + "ram": "RAM" + }, + "dashboardTab": { + "disk": "Korong" + }, + "newUi": { + "sidebar": { + "snippets": { + "cancel": "Mégsem", + "confirmRunButton": "Fut", + "confirmRunMessage": "Futtassa a \"{{name}} \" programot?" + } + } + }, + "terminal": { + "connected": "Csatlakoztatva" } } diff --git a/plugins/host-metrics/locales/translated/id_ID.json b/plugins/host-metrics/locales/translated/id_ID.json index 03a9429f3..646f1d2e3 100644 --- a/plugins/host-metrics/locales/translated/id_ID.json +++ b/plugins/host-metrics/locales/translated/id_ID.json @@ -240,7 +240,9 @@ "noQuickActions": "Belum ada tindakan cepat.", "buttonLabel": "Label tombol", "selectSnippetPlaceholder": "Pilih cuplikan...", - "addActionBtn": "Tambahkan Tindakan" + "addActionBtn": "Tambahkan Tindakan", + "hostNotFound": "Host tidak ditemukan", + "loadingHost": "Memuat host..." }, "onboarding": { "feature_metrics": "Metrik dan peringatan", @@ -255,6 +257,41 @@ "noData": "Tidak ada data yang tersedia", "showCurrentValue": "Tampilkan Nilai Saat Ini", "chartMetric": "Metrik", - "metricRange": "Jangkauan" + "metricRange": "Jangkauan", + "host": "Tuan rumah", + "loading": "Memuat...", + "metricCpu": "CPU", + "metricDisk": "Disk", + "metricHostname": "Nama host", + "metricKernel": "Inti", + "metricMemory": "Ingatan", + "metricNetwork": "Jaringan", + "metricOs": "Sistem Operasi", + "metricProcesses": "Proses", + "metricProcessesRunning": "berlari", + "metricProcessesTotal": "total", + "metricUptime": "Waktu aktif", + "metricsNotAvailable": "Metrik tidak tersedia", + "noHostSelected": "Tidak ada pembawa acara yang dipilih.", + "selectHost": "Pilih tuan rumah..." + }, + "dashboard": { + "cpu": "CPU", + "ram": "RAM" + }, + "dashboardTab": { + "disk": "Disk" + }, + "newUi": { + "sidebar": { + "snippets": { + "cancel": "Membatalkan", + "confirmRunButton": "Berlari", + "confirmRunMessage": "Jalankan \"{{name}}\"?" + } + } + }, + "terminal": { + "connected": "Terhubung" } } diff --git a/plugins/host-metrics/locales/translated/it_IT.json b/plugins/host-metrics/locales/translated/it_IT.json index 809d0d259..ce65596e7 100644 --- a/plugins/host-metrics/locales/translated/it_IT.json +++ b/plugins/host-metrics/locales/translated/it_IT.json @@ -240,7 +240,9 @@ "noQuickActions": "Nessuna azione rapida ancora.", "buttonLabel": "Etichetta pulsante", "selectSnippetPlaceholder": "Seleziona snippet...", - "addActionBtn": "Aggiungi Azione" + "addActionBtn": "Aggiungi Azione", + "hostNotFound": "Host non trovato", + "loadingHost": "Caricamento host..." }, "onboarding": { "feature_metrics": "Metriche e avvisi", @@ -255,6 +257,41 @@ "noData": "Nessun dato disponibile", "showCurrentValue": "Mostra valore corrente", "chartMetric": "Metrica", - "metricRange": "Intervallo" + "metricRange": "Intervallo", + "host": "Host", + "loading": "Caricamento...", + "metricCpu": "CPU", + "metricDisk": "Disco", + "metricHostname": "Nome host", + "metricKernel": "Kernel", + "metricMemory": "Memoria", + "metricNetwork": "Rete", + "metricOs": "OS", + "metricProcesses": "Processi", + "metricProcessesRunning": "in esecuzione", + "metricProcessesTotal": "totale", + "metricUptime": "Uptime", + "metricsNotAvailable": "Metriche non disponibili", + "noHostSelected": "Nessun host selezionato", + "selectHost": "Seleziona un host..." + }, + "dashboard": { + "cpu": "CPU", + "ram": "RAM" + }, + "dashboardTab": { + "disk": "Disco" + }, + "newUi": { + "sidebar": { + "snippets": { + "cancel": "Annulla", + "confirmRunButton": "Esegui", + "confirmRunMessage": "Eseguire \"{{name}}\"?" + } + } + }, + "terminal": { + "connected": "Connesso" } } diff --git a/plugins/host-metrics/locales/translated/ja_JP.json b/plugins/host-metrics/locales/translated/ja_JP.json index f0e2cfd10..2dafa5075 100644 --- a/plugins/host-metrics/locales/translated/ja_JP.json +++ b/plugins/host-metrics/locales/translated/ja_JP.json @@ -240,7 +240,9 @@ "noQuickActions": "クイックアクションはまだありません", "buttonLabel": "ボタンラベル", "selectSnippetPlaceholder": "スニペットを選択...", - "addActionBtn": "アクションを追加" + "addActionBtn": "アクションを追加", + "hostNotFound": "ホストが見つかりません", + "loadingHost": "ホストを読み込み中..." }, "onboarding": { "feature_metrics": "指標とアラート", @@ -255,6 +257,41 @@ "noData": "利用可能なデータはありません", "showCurrentValue": "現在の値を表示", "chartMetric": "メトリック", - "metricRange": "範囲" + "metricRange": "範囲", + "host": "ホスト", + "loading": "読み込み中...", + "metricCpu": "CPU", + "metricDisk": "ディスク", + "metricHostname": "ホスト名", + "metricKernel": "カーネル", + "metricMemory": "メモリ", + "metricNetwork": "ネットワーク", + "metricOs": "OS", + "metricProcesses": "プロセス", + "metricProcessesRunning": "実行中", + "metricProcessesTotal": "合計", + "metricUptime": "稼働時間", + "metricsNotAvailable": "メトリクスは利用できません", + "noHostSelected": "ホストが選択されていません", + "selectHost": "ホストを選択..." + }, + "dashboard": { + "cpu": "CPU", + "ram": "RAM" + }, + "dashboardTab": { + "disk": "ディスク" + }, + "newUi": { + "sidebar": { + "snippets": { + "cancel": "キャンセル", + "confirmRunButton": "実行", + "confirmRunMessage": "「{{name}}」を実行しますか?" + } + } + }, + "terminal": { + "connected": "接続中" } } diff --git a/plugins/host-metrics/locales/translated/ko_KR.json b/plugins/host-metrics/locales/translated/ko_KR.json index f35781a7b..2cc9ba92d 100644 --- a/plugins/host-metrics/locales/translated/ko_KR.json +++ b/plugins/host-metrics/locales/translated/ko_KR.json @@ -240,7 +240,9 @@ "noQuickActions": "아직 빠른 작업이 없습니다.", "buttonLabel": "버튼 레이블", "selectSnippetPlaceholder": "스니펫 선택...", - "addActionBtn": "작업 추가" + "addActionBtn": "작업 추가", + "hostNotFound": "호스트를 찾을 수 없습니다", + "loadingHost": "호스트 로드 중..." }, "onboarding": { "feature_metrics": "지표 및 알림", @@ -255,6 +257,41 @@ "noData": "데이터 없음", "showCurrentValue": "현재 값 표시", "chartMetric": "메트릭", - "metricRange": "범위" + "metricRange": "범위", + "host": "호스트", + "loading": "로딩 중...", + "metricCpu": "CPU", + "metricDisk": "디스크", + "metricHostname": "호스트명", + "metricKernel": "커널", + "metricMemory": "메모리", + "metricNetwork": "네트워크", + "metricOs": "OS", + "metricProcesses": "프로세스", + "metricProcessesRunning": "실행 중", + "metricProcessesTotal": "전체", + "metricUptime": "업타임", + "metricsNotAvailable": "메트릭을 사용할 수 없습니다", + "noHostSelected": "선택된 호스트 없음", + "selectHost": "호스트 선택..." + }, + "dashboard": { + "cpu": "CPU", + "ram": "RAM" + }, + "dashboardTab": { + "disk": "디스크" + }, + "newUi": { + "sidebar": { + "snippets": { + "cancel": "취소", + "confirmRunButton": "실행", + "confirmRunMessage": "\"{{name}}\"을(를) 실행하시겠습니까?" + } + } + }, + "terminal": { + "connected": "연결됨" } } diff --git a/plugins/host-metrics/locales/translated/nl_NL.json b/plugins/host-metrics/locales/translated/nl_NL.json index 156025187..fb37c8c92 100644 --- a/plugins/host-metrics/locales/translated/nl_NL.json +++ b/plugins/host-metrics/locales/translated/nl_NL.json @@ -240,7 +240,9 @@ "noQuickActions": "Nog geen snelle acties ondernomen.", "buttonLabel": "Knoplabel", "selectSnippetPlaceholder": "Selecteer fragment...", - "addActionBtn": "Actie toevoegen" + "addActionBtn": "Actie toevoegen", + "hostNotFound": "Host niet gevonden", + "loadingHost": "Host wordt geladen..." }, "onboarding": { "feature_metrics": "Statistieken en waarschuwingen", @@ -255,6 +257,41 @@ "noData": "Geen gegevens beschikbaar", "showCurrentValue": "Toon huidige waarde", "chartMetric": "Metrisch", - "metricRange": "Bereik" + "metricRange": "Bereik", + "host": "Gastheer", + "loading": "Laden...", + "metricCpu": "CPU", + "metricDisk": "Schijf", + "metricHostname": "Hostnaam", + "metricKernel": "Kernel", + "metricMemory": "Geheugen", + "metricNetwork": "Netwerk", + "metricOs": "OS", + "metricProcesses": "Processen", + "metricProcessesRunning": "rennen", + "metricProcessesTotal": "totaal", + "metricUptime": "Beschikbaarheid", + "metricsNotAvailable": "Gegevens niet beschikbaar", + "noHostSelected": "Geen host geselecteerd", + "selectHost": "Selecteer een host..." + }, + "dashboard": { + "cpu": "CPU", + "ram": "RAM" + }, + "dashboardTab": { + "disk": "Schijf" + }, + "newUi": { + "sidebar": { + "snippets": { + "cancel": "Annuleren", + "confirmRunButton": "Loop", + "confirmRunMessage": "Run \"{{name}}\"?" + } + } + }, + "terminal": { + "connected": "Aangesloten" } } diff --git a/plugins/host-metrics/locales/translated/no_NO.json b/plugins/host-metrics/locales/translated/no_NO.json index bdfa5cb02..9ea9715dd 100644 --- a/plugins/host-metrics/locales/translated/no_NO.json +++ b/plugins/host-metrics/locales/translated/no_NO.json @@ -240,7 +240,9 @@ "noQuickActions": "Ingen hurtighandlinger ennå.", "buttonLabel": "Knappetikett", "selectSnippetPlaceholder": "Velg utdrag...", - "addActionBtn": "Legg til handling" + "addActionBtn": "Legg til handling", + "hostNotFound": "Verten ble ikke funnet", + "loadingHost": "Laster inn vert ..." }, "onboarding": { "feature_metrics": "Målinger og varsler", @@ -255,6 +257,41 @@ "noData": "Ingen data tilgjengelig", "showCurrentValue": "Vis gjeldende verdi", "chartMetric": "Metrisk", - "metricRange": "Spekter" + "metricRange": "Spekter", + "host": "Vert", + "loading": "Laster inn...", + "metricCpu": "CPU", + "metricDisk": "Disk", + "metricHostname": "Vertsnavn", + "metricKernel": "Kjerne", + "metricMemory": "Hukommelse", + "metricNetwork": "Nettverk", + "metricOs": "OS", + "metricProcesses": "Prosesser", + "metricProcessesRunning": "løping", + "metricProcessesTotal": "total", + "metricUptime": "Oppetid", + "metricsNotAvailable": "Målinger er ikke tilgjengelige", + "noHostSelected": "Ingen vert valgt", + "selectHost": "Velg en vert..." + }, + "dashboard": { + "cpu": "CPU", + "ram": "VÆR" + }, + "dashboardTab": { + "disk": "Disk" + }, + "newUi": { + "sidebar": { + "snippets": { + "cancel": "Kansellere", + "confirmRunButton": "Løp", + "confirmRunMessage": "Kjør \"{{name}}\"?" + } + } + }, + "terminal": { + "connected": "Tilkoblet" } } diff --git a/plugins/host-metrics/locales/translated/pl_PL.json b/plugins/host-metrics/locales/translated/pl_PL.json index bdcac6e3f..a3f1a10c3 100644 --- a/plugins/host-metrics/locales/translated/pl_PL.json +++ b/plugins/host-metrics/locales/translated/pl_PL.json @@ -240,7 +240,9 @@ "noQuickActions": "Brak szybkich akcji.", "buttonLabel": "Etykieta przycisku", "selectSnippetPlaceholder": "Wybierz snippet...", - "addActionBtn": "Dodaj akcję" + "addActionBtn": "Dodaj akcję", + "hostNotFound": "Nie znaleziono hosta", + "loadingHost": "Ładowanie hosta..." }, "onboarding": { "feature_metrics": "Metryki i alerty", @@ -255,6 +257,41 @@ "noData": "Brak dostępnych danych", "showCurrentValue": "Pokaż wartość bieżącą", "chartMetric": "Metryka", - "metricRange": "Zakres" + "metricRange": "Zakres", + "host": "Host", + "loading": "Ładowanie...", + "metricCpu": "Procesor", + "metricDisk": "Dysk", + "metricHostname": "Nazwa hosta", + "metricKernel": "Jądro", + "metricMemory": "Pamięć", + "metricNetwork": "Sieć", + "metricOs": "System operacyjny", + "metricProcesses": "Procesy", + "metricProcessesRunning": "działa", + "metricProcessesTotal": "łącznie", + "metricUptime": "Czas działania", + "metricsNotAvailable": "Metryki niedostępne", + "noHostSelected": "Nie wybrano hosta", + "selectHost": "Wybierz hosta..." + }, + "dashboard": { + "cpu": "Procesor", + "ram": "RAM" + }, + "dashboardTab": { + "disk": "Dysk" + }, + "newUi": { + "sidebar": { + "snippets": { + "cancel": "Anuluj", + "confirmRunButton": "Uruchom", + "confirmRunMessage": "Uruchomić „{{name}}”?" + } + } + }, + "terminal": { + "connected": "Połączono" } } diff --git a/plugins/host-metrics/locales/translated/pt_BR.json b/plugins/host-metrics/locales/translated/pt_BR.json index 994b5117e..4c7343950 100644 --- a/plugins/host-metrics/locales/translated/pt_BR.json +++ b/plugins/host-metrics/locales/translated/pt_BR.json @@ -240,7 +240,9 @@ "noQuickActions": "Nenhuma ação imediata ainda.", "buttonLabel": "Rótulo do botão", "selectSnippetPlaceholder": "Selecione o trecho...", - "addActionBtn": "Adicionar ação" + "addActionBtn": "Adicionar ação", + "hostNotFound": "Host não encontrado", + "loadingHost": "Carregando host..." }, "onboarding": { "feature_metrics": "Métricas e alertas", @@ -255,6 +257,41 @@ "noData": "Não há dados disponíveis.", "showCurrentValue": "Mostrar valor atual", "chartMetric": "Métrica", - "metricRange": "Faixa" + "metricRange": "Faixa", + "host": "Hospedar", + "loading": "Carregando...", + "metricCpu": "CPU", + "metricDisk": "Disco", + "metricHostname": "Nome do host", + "metricKernel": "Kernel", + "metricMemory": "Memória", + "metricNetwork": "Rede", + "metricOs": "SO", + "metricProcesses": "Processos", + "metricProcessesRunning": "correndo", + "metricProcessesTotal": "total", + "metricUptime": "Tempo de atividade", + "metricsNotAvailable": "Métricas não disponíveis", + "noHostSelected": "Nenhum host selecionado", + "selectHost": "Selecione um host..." + }, + "dashboard": { + "cpu": "CPU", + "ram": "BATER" + }, + "dashboardTab": { + "disk": "Disco" + }, + "newUi": { + "sidebar": { + "snippets": { + "cancel": "Cancelar", + "confirmRunButton": "Correr", + "confirmRunMessage": "Executar \"{{name}}\"?" + } + } + }, + "terminal": { + "connected": "Conectado" } } diff --git a/plugins/host-metrics/locales/translated/pt_PT.json b/plugins/host-metrics/locales/translated/pt_PT.json index d25c2175a..108a76e10 100644 --- a/plugins/host-metrics/locales/translated/pt_PT.json +++ b/plugins/host-metrics/locales/translated/pt_PT.json @@ -240,7 +240,9 @@ "noQuickActions": "Ainda não existem ações rápidas.", "buttonLabel": "Texto do botão", "selectSnippetPlaceholder": "Selecionar snippet...", - "addActionBtn": "Adicionar Ação" + "addActionBtn": "Adicionar Ação", + "hostNotFound": "Host não encontrado", + "loadingHost": "A carregar o host..." }, "onboarding": { "feature_metrics": "Métricas e alertas", @@ -255,6 +257,41 @@ "noData": "Sem dados disponíveis", "showCurrentValue": "Mostrar valor atual", "chartMetric": "Métrica", - "metricRange": "Intervalo" + "metricRange": "Intervalo", + "host": "Host", + "loading": "A carregar...", + "metricCpu": "CPU", + "metricDisk": "Disco", + "metricHostname": "Nome do host", + "metricKernel": "Kernel", + "metricMemory": "Memória", + "metricNetwork": "Rede", + "metricOs": "SO", + "metricProcesses": "Processos", + "metricProcessesRunning": "em execução", + "metricProcessesTotal": "total", + "metricUptime": "Tempo de atividade", + "metricsNotAvailable": "Métricas não disponíveis", + "noHostSelected": "Nenhum anfitrião selecionado", + "selectHost": "Selecionar um host..." + }, + "dashboard": { + "cpu": "CPU", + "ram": "RAM" + }, + "dashboardTab": { + "disk": "Disco" + }, + "newUi": { + "sidebar": { + "snippets": { + "cancel": "Cancelar", + "confirmRunButton": "Executar", + "confirmRunMessage": "Executar \"{{name}}\"?" + } + } + }, + "terminal": { + "connected": "Ligado" } } diff --git a/plugins/host-metrics/locales/translated/ro_RO.json b/plugins/host-metrics/locales/translated/ro_RO.json index 3508bc622..454a5a0d1 100644 --- a/plugins/host-metrics/locales/translated/ro_RO.json +++ b/plugins/host-metrics/locales/translated/ro_RO.json @@ -240,7 +240,9 @@ "noQuickActions": "Nicio acțiune rapidă încă.", "buttonLabel": "Etichetă buton", "selectSnippetPlaceholder": "Selectați fragmentul...", - "addActionBtn": "Adăugați o acțiune" + "addActionBtn": "Adăugați o acțiune", + "hostNotFound": "Gazdă negăsită", + "loadingHost": "Se încarcă gazda..." }, "onboarding": { "feature_metrics": "Metrici și alerte", @@ -255,6 +257,41 @@ "noData": "Nu sunt disponibile date", "showCurrentValue": "Afișează valoarea curentă", "chartMetric": "Metric", - "metricRange": "Gamă" + "metricRange": "Gamă", + "host": "Gazdă", + "loading": "Încărcare...", + "metricCpu": "CPU", + "metricDisk": "Disc", + "metricHostname": "Nume gazdă", + "metricKernel": "Nucleu", + "metricMemory": "Memorie", + "metricNetwork": "Reţea", + "metricOs": "Sistem de operare", + "metricProcesses": "Procese", + "metricProcessesRunning": "funcţionare", + "metricProcessesTotal": "total", + "metricUptime": "Timp de funcționare", + "metricsNotAvailable": "Indicatorii nu sunt disponibili", + "noHostSelected": "Nicio gazdă selectată", + "selectHost": "Selectați o gazdă..." + }, + "dashboard": { + "cpu": "CPU", + "ram": "RAM" + }, + "dashboardTab": { + "disk": "Disc" + }, + "newUi": { + "sidebar": { + "snippets": { + "cancel": "Anula", + "confirmRunButton": "Aleargă", + "confirmRunMessage": "Executați „{{name}}”?" + } + } + }, + "terminal": { + "connected": "Conectat" } } diff --git a/plugins/host-metrics/locales/translated/ru_RU.json b/plugins/host-metrics/locales/translated/ru_RU.json index 92bd151b5..ce1049e18 100644 --- a/plugins/host-metrics/locales/translated/ru_RU.json +++ b/plugins/host-metrics/locales/translated/ru_RU.json @@ -240,7 +240,9 @@ "noQuickActions": "Пока нет быстрых действий.", "buttonLabel": "Надпись кнопки", "selectSnippetPlaceholder": "Выбрать сниппет...", - "addActionBtn": "Добавить действие" + "addActionBtn": "Добавить действие", + "hostNotFound": "Хост не найден", + "loadingHost": "Загрузка хоста..." }, "onboarding": { "feature_metrics": "Метрики и оповещения", @@ -255,6 +257,41 @@ "noData": "Нет данных", "showCurrentValue": "Показать текущее значение", "chartMetric": "Метрика", - "metricRange": "Диапазон" + "metricRange": "Диапазон", + "host": "Хост", + "loading": "Загрузка...", + "metricCpu": "ЦП", + "metricDisk": "Диск", + "metricHostname": "Имя хоста", + "metricKernel": "Ядро", + "metricMemory": "Память", + "metricNetwork": "Сеть", + "metricOs": "ОС", + "metricProcesses": "Процессы", + "metricProcessesRunning": "запущено", + "metricProcessesTotal": "всего", + "metricUptime": "Время работы", + "metricsNotAvailable": "Метрики недоступны", + "noHostSelected": "Хост не выбран", + "selectHost": "Выберите хост..." + }, + "dashboard": { + "cpu": "CPU", + "ram": "RAM" + }, + "dashboardTab": { + "disk": "Диск" + }, + "newUi": { + "sidebar": { + "snippets": { + "cancel": "Отмена", + "confirmRunButton": "Запустить", + "confirmRunMessage": "Запустить \"{{name}}\"?" + } + } + }, + "terminal": { + "connected": "Подключено" } } diff --git a/plugins/host-metrics/locales/translated/sr_SP.json b/plugins/host-metrics/locales/translated/sr_SP.json index bb11cfeb3..a3dd81af5 100644 --- a/plugins/host-metrics/locales/translated/sr_SP.json +++ b/plugins/host-metrics/locales/translated/sr_SP.json @@ -240,7 +240,9 @@ "noQuickActions": "Још нема брзих акција.", "buttonLabel": "Ознака дугмета", "selectSnippetPlaceholder": "Изаберите исечак...", - "addActionBtn": "Додај акцију" + "addActionBtn": "Додај акцију", + "hostNotFound": "Хост није пронађен", + "loadingHost": "Учитавање хоста..." }, "onboarding": { "feature_metrics": "Метрике и упозорења", @@ -255,6 +257,41 @@ "noData": "Нема доступних података", "showCurrentValue": "Прикажи тренутну вредност", "chartMetric": "Метрика", - "metricRange": "Домет" + "metricRange": "Домет", + "host": "Домаћин", + "loading": "Учитавање...", + "metricCpu": "Процесор", + "metricDisk": "Диск", + "metricHostname": "Име хоста", + "metricKernel": "Језгро", + "metricMemory": "Памћење", + "metricNetwork": "Мрежа", + "metricOs": "ОС", + "metricProcesses": "Процеси", + "metricProcessesRunning": "трчање", + "metricProcessesTotal": "укупно", + "metricUptime": "Време непрекидног рада", + "metricsNotAvailable": "Метрике нису доступне", + "noHostSelected": "Није изабран домаћин", + "selectHost": "Изаберите хост..." + }, + "dashboard": { + "cpu": "Процесор", + "ram": "РАМ меморија" + }, + "dashboardTab": { + "disk": "Диск" + }, + "newUi": { + "sidebar": { + "snippets": { + "cancel": "Откажи", + "confirmRunButton": "Трчи", + "confirmRunMessage": "Покрени „{{name}}“?" + } + } + }, + "terminal": { + "connected": "Повезано" } } diff --git a/plugins/host-metrics/locales/translated/sv_SE.json b/plugins/host-metrics/locales/translated/sv_SE.json index f5678030b..f0e52eb54 100644 --- a/plugins/host-metrics/locales/translated/sv_SE.json +++ b/plugins/host-metrics/locales/translated/sv_SE.json @@ -240,7 +240,9 @@ "noQuickActions": "Inga snabba åtgärder ännu.", "buttonLabel": "Knappetikett", "selectSnippetPlaceholder": "Välj utdrag...", - "addActionBtn": "Lägg till åtgärd" + "addActionBtn": "Lägg till åtgärd", + "hostNotFound": "Värden hittades inte", + "loadingHost": "Laddar värd..." }, "onboarding": { "feature_metrics": "Mätvärden och varningar", @@ -255,6 +257,41 @@ "noData": "Inga data tillgängliga", "showCurrentValue": "Visa aktuellt värde", "chartMetric": "Metrisk", - "metricRange": "Räckvidd" + "metricRange": "Räckvidd", + "host": "Värd", + "loading": "Belastning...", + "metricCpu": "CPU", + "metricDisk": "Disk", + "metricHostname": "Värdnamn", + "metricKernel": "Kärna", + "metricMemory": "Minne", + "metricNetwork": "Nätverk", + "metricOs": "operativsystem", + "metricProcesses": "Processer", + "metricProcessesRunning": "spring", + "metricProcessesTotal": "total", + "metricUptime": "Drifttid", + "metricsNotAvailable": "Mätvärden är inte tillgängliga", + "noHostSelected": "Ingen värd vald", + "selectHost": "Välj en värd..." + }, + "dashboard": { + "cpu": "CPU", + "ram": "RAM-minne" + }, + "dashboardTab": { + "disk": "Disk" + }, + "newUi": { + "sidebar": { + "snippets": { + "cancel": "Avboka", + "confirmRunButton": "Sikt", + "confirmRunMessage": "Kör \"{{name}}\"?" + } + } + }, + "terminal": { + "connected": "Ansluten" } } diff --git a/plugins/host-metrics/locales/translated/th_TH.json b/plugins/host-metrics/locales/translated/th_TH.json index 8fb8c9698..4dacdc78a 100644 --- a/plugins/host-metrics/locales/translated/th_TH.json +++ b/plugins/host-metrics/locales/translated/th_TH.json @@ -240,7 +240,9 @@ "noQuickActions": "ยังไม่มีการดำเนินการใดๆ อย่างเร่งด่วนในขณะนี้", "buttonLabel": "ป้ายกำกับปุ่ม", "selectSnippetPlaceholder": "เลือกส่วนย่อย...", - "addActionBtn": "เพิ่มการดำเนินการ" + "addActionBtn": "เพิ่มการดำเนินการ", + "hostNotFound": "ไม่พบโฮสต์", + "loadingHost": "กำลังโหลดโฮสต์..." }, "onboarding": { "feature_metrics": "ตัวชี้วัดและการแจ้งเตือน", @@ -255,6 +257,41 @@ "noData": "ไม่มีข้อมูล", "showCurrentValue": "แสดงมูลค่าปัจจุบัน", "chartMetric": "เมตริก", - "metricRange": "พิสัย" + "metricRange": "พิสัย", + "host": "เจ้าภาพ", + "loading": "กำลังโหลด...", + "metricCpu": "ซีพียู", + "metricDisk": "ดิสก์", + "metricHostname": "ชื่อโฮสต์", + "metricKernel": "เคอร์เนล", + "metricMemory": "หน่วยความจำ", + "metricNetwork": "เครือข่าย", + "metricOs": "โอเอส", + "metricProcesses": "กระบวนการ", + "metricProcessesRunning": "วิ่ง", + "metricProcessesTotal": "ทั้งหมด", + "metricUptime": "เวลาใช้งาน", + "metricsNotAvailable": "ข้อมูลตัวชี้วัดไม่พร้อมใช้งาน", + "noHostSelected": "ไม่ได้เลือกโฮสต์", + "selectHost": "เลือกโฮสต์..." + }, + "dashboard": { + "cpu": "ซีพียู", + "ram": "แรม" + }, + "dashboardTab": { + "disk": "ดิสก์" + }, + "newUi": { + "sidebar": { + "snippets": { + "cancel": "ยกเลิก", + "confirmRunButton": "วิ่ง", + "confirmRunMessage": "เรียกใช้ \"{{name}}\"?" + } + } + }, + "terminal": { + "connected": "เชื่อมต่อแล้ว" } } diff --git a/plugins/host-metrics/locales/translated/tr_TR.json b/plugins/host-metrics/locales/translated/tr_TR.json index 25583df1b..c51215ce0 100644 --- a/plugins/host-metrics/locales/translated/tr_TR.json +++ b/plugins/host-metrics/locales/translated/tr_TR.json @@ -240,7 +240,9 @@ "noQuickActions": "Henüz hızlı eylem yok.", "buttonLabel": "Düğme etiketi", "selectSnippetPlaceholder": "Parçacık seç...", - "addActionBtn": "Eylem Ekle" + "addActionBtn": "Eylem Ekle", + "hostNotFound": "Host bulunamadı", + "loadingHost": "Host yükleniyor..." }, "onboarding": { "feature_metrics": "Ölçümler ve uyarılar", @@ -255,6 +257,41 @@ "noData": "Veri yok", "showCurrentValue": "Geçerli Değeri Göster", "chartMetric": "Metrik", - "metricRange": "Aralık" + "metricRange": "Aralık", + "host": "Sunucu", + "loading": "Yükleniyor...", + "metricCpu": "CPU", + "metricDisk": "Disk", + "metricHostname": "Ana Bilgisayar Adı", + "metricKernel": "Çekirdek", + "metricMemory": "Bellek", + "metricNetwork": "Ağ", + "metricOs": "İşletim Sistemi", + "metricProcesses": "İşlemler", + "metricProcessesRunning": "çalışan", + "metricProcessesTotal": "toplam", + "metricUptime": "Çalışma Süresi", + "metricsNotAvailable": "Metrikler kullanılamıyor", + "noHostSelected": "Sunucu seçilmedi", + "selectHost": "Bir sunucu seçin..." + }, + "dashboard": { + "cpu": "CPU", + "ram": "RAM" + }, + "dashboardTab": { + "disk": "Disk" + }, + "newUi": { + "sidebar": { + "snippets": { + "cancel": "İptal", + "confirmRunButton": "Çalıştır", + "confirmRunMessage": "\"{{name}}\" çalıştırılsın mı?" + } + } + }, + "terminal": { + "connected": "Bağlandı" } } diff --git a/plugins/host-metrics/locales/translated/uk_UA.json b/plugins/host-metrics/locales/translated/uk_UA.json index b035bda64..2c66178ba 100644 --- a/plugins/host-metrics/locales/translated/uk_UA.json +++ b/plugins/host-metrics/locales/translated/uk_UA.json @@ -240,7 +240,9 @@ "noQuickActions": "Поки що жодних швидких дій.", "buttonLabel": "Мітка кнопки", "selectSnippetPlaceholder": "Вибрати фрагмент...", - "addActionBtn": "Додати дію" + "addActionBtn": "Додати дію", + "hostNotFound": "Хост не знайдено", + "loadingHost": "Завантаження хоста..." }, "onboarding": { "feature_metrics": "Метрики та сповіщення", @@ -255,6 +257,41 @@ "noData": "Дані недоступні", "showCurrentValue": "Показати поточне значення", "chartMetric": "Метрика", - "metricRange": "Діапазон" + "metricRange": "Діапазон", + "host": "Хост", + "loading": "Завантаження...", + "metricCpu": "Процесор", + "metricDisk": "Диск", + "metricHostname": "Ім'я хоста", + "metricKernel": "Ядро", + "metricMemory": "Пам'ять", + "metricNetwork": "Мережа", + "metricOs": "ОС", + "metricProcesses": "Процеси", + "metricProcessesRunning": "біг", + "metricProcessesTotal": "загальна сума", + "metricUptime": "Час безперебійної роботи", + "metricsNotAvailable": "Метрики недоступні", + "noHostSelected": "Хост не вибрано", + "selectHost": "Виберіть хоста..." + }, + "dashboard": { + "cpu": "Процесор", + "ram": "Оперативна пам'ять" + }, + "dashboardTab": { + "disk": "Диск" + }, + "newUi": { + "sidebar": { + "snippets": { + "cancel": "Скасувати", + "confirmRunButton": "Бігти", + "confirmRunMessage": "Запустити \"{{name}}\"?" + } + } + }, + "terminal": { + "connected": "Підключено" } } diff --git a/plugins/host-metrics/locales/translated/vi_VN.json b/plugins/host-metrics/locales/translated/vi_VN.json index ac081f331..53828e648 100644 --- a/plugins/host-metrics/locales/translated/vi_VN.json +++ b/plugins/host-metrics/locales/translated/vi_VN.json @@ -240,7 +240,9 @@ "noQuickActions": "Chưa có hành động nhanh nào.", "buttonLabel": "Nhãn nút", "selectSnippetPlaceholder": "Chọn đoạn mã...", - "addActionBtn": "Thêm hành động" + "addActionBtn": "Thêm hành động", + "hostNotFound": "Không tìm thấy máy chủ", + "loadingHost": "Đang tải máy chủ..." }, "onboarding": { "feature_metrics": "Số liệu và cảnh báo", @@ -255,6 +257,41 @@ "noData": "Không có dữ liệu", "showCurrentValue": "Hiển thị giá trị hiện tại", "chartMetric": "Chỉ số", - "metricRange": "Phạm vi" + "metricRange": "Phạm vi", + "host": "Máy chủ", + "loading": "Đang tải...", + "metricCpu": "CPU", + "metricDisk": "Đĩa", + "metricHostname": "Tên máy chủ", + "metricKernel": "Nhân", + "metricMemory": "Bộ nhớ", + "metricNetwork": "Mạng", + "metricOs": "Hệ điều hành", + "metricProcesses": "Tiến trình", + "metricProcessesRunning": "đang chạy", + "metricProcessesTotal": "tổng", + "metricUptime": "Thời gian hoạt động", + "metricsNotAvailable": "Chỉ số không khả dụng", + "noHostSelected": "Chưa chọn máy chủ", + "selectHost": "Chọn máy chủ..." + }, + "dashboard": { + "cpu": "CPU", + "ram": "RAM" + }, + "dashboardTab": { + "disk": "Ổ đĩa" + }, + "newUi": { + "sidebar": { + "snippets": { + "cancel": "Hủy", + "confirmRunButton": "Chạy", + "confirmRunMessage": "Chạy \"{{name}}\"?" + } + } + }, + "terminal": { + "connected": "Đã kết nối" } } diff --git a/plugins/host-metrics/locales/translated/zh_CN.json b/plugins/host-metrics/locales/translated/zh_CN.json index dab6f81aa..9736ea220 100644 --- a/plugins/host-metrics/locales/translated/zh_CN.json +++ b/plugins/host-metrics/locales/translated/zh_CN.json @@ -240,7 +240,9 @@ "noQuickActions": "尚无快速操作。", "buttonLabel": "按钮标签", "selectSnippetPlaceholder": "选择代码片段...", - "addActionBtn": "添加操作" + "addActionBtn": "添加操作", + "hostNotFound": "未找到主机", + "loadingHost": "正在加载主机..." }, "onboarding": { "feature_metrics": "指标与告警", @@ -255,6 +257,41 @@ "noData": "暂无数据", "showCurrentValue": "显示当前值", "chartMetric": "指标", - "metricRange": "范围" + "metricRange": "范围", + "host": "主机", + "loading": "加载中...", + "metricCpu": "CPU", + "metricDisk": "磁盘", + "metricHostname": "主机名", + "metricKernel": "内核", + "metricMemory": "内存", + "metricNetwork": "网络", + "metricOs": "操作系统", + "metricProcesses": "进程", + "metricProcessesRunning": "运行中", + "metricProcessesTotal": "总数", + "metricUptime": "运行时间", + "metricsNotAvailable": "指标不可用", + "noHostSelected": "未选择主机", + "selectHost": "选择主机..." + }, + "dashboard": { + "cpu": "CPU", + "ram": "内存" + }, + "dashboardTab": { + "disk": "磁盘" + }, + "newUi": { + "sidebar": { + "snippets": { + "cancel": "取消", + "confirmRunButton": "运行", + "confirmRunMessage": "运行“{{name}}”?" + } + } + }, + "terminal": { + "connected": "已连接" } } diff --git a/plugins/host-metrics/locales/translated/zh_TW.json b/plugins/host-metrics/locales/translated/zh_TW.json index 6a4610531..db704c54c 100644 --- a/plugins/host-metrics/locales/translated/zh_TW.json +++ b/plugins/host-metrics/locales/translated/zh_TW.json @@ -240,7 +240,9 @@ "noQuickActions": "尚無快速動作。", "buttonLabel": "按鈕標籤", "selectSnippetPlaceholder": "選取程式碼片段...", - "addActionBtn": "新增動作" + "addActionBtn": "新增動作", + "hostNotFound": "找不到主機", + "loadingHost": "正在載入主機..." }, "onboarding": { "feature_metrics": "指標和警報", @@ -255,6 +257,41 @@ "noData": "無可用資料", "showCurrentValue": "顯示目前值", "chartMetric": "指標", - "metricRange": "範圍" + "metricRange": "範圍", + "host": "主機", + "loading": "載入中...", + "metricCpu": "CPU", + "metricDisk": "磁碟", + "metricHostname": "主機名稱", + "metricKernel": "核心", + "metricMemory": "記憶體", + "metricNetwork": "網路", + "metricOs": "作業系統", + "metricProcesses": "程序", + "metricProcessesRunning": "執行中", + "metricProcessesTotal": "總數", + "metricUptime": "運作時間", + "metricsNotAvailable": "效能指標無法使用", + "noHostSelected": "未選取主機", + "selectHost": "選擇主機..." + }, + "dashboard": { + "cpu": "CPU", + "ram": "RAM" + }, + "dashboardTab": { + "disk": "磁碟" + }, + "newUi": { + "sidebar": { + "snippets": { + "cancel": "取消", + "confirmRunButton": "執行", + "confirmRunMessage": "執行 \"{{name}}\"?" + } + } + }, + "terminal": { + "connected": "已連線" } } diff --git a/plugins/host-metrics/manifest.json b/plugins/host-metrics/manifest.json index 378677b13..3841be3aa 100644 --- a/plugins/host-metrics/manifest.json +++ b/plugins/host-metrics/manifest.json @@ -2,7 +2,7 @@ "id": "host-metrics", "name": "Host Metrics", "version": "1.0.0", - "description": "Live CPU, memory, disk, network and process metrics, plus host management (services, packages, firewall, cron, users, SSL, logs, WireGuard, Tailscale).", + "description": "Live CPU, memory, disk, network and process metrics, plus host management (services, packages, firewall, cron, users, SSL, logs, WireGuard).", "author": { "name": "Termix" }, @@ -20,12 +20,15 @@ "ssh:connect", "db:own", "network:serve", - "events:core", "ui:surface" ], - "optionalDependencies": { - "automations": "^1.0.0" - }, + "provides": [ + { + "service": "host-metrics.viewers", + "version": "1.0.0", + "permission": "host-metrics.use" + } + ], "contributes": { "tabs": [ { @@ -35,14 +38,98 @@ "openFrom": ["host-context-menu", "palette"] } ], + "permissions": [ + { + "name": "use", + "titleKey": "permissions.use.title", + "descriptionKey": "permissions.use.description", + "defaultRoles": ["admin", "user"] + } + ], "settings": { "admin": [ { - "key": "historyRetention", - "type": "custom", - "component": "retention" + "key": "metricsInterval", + "type": "number", + "labelKey": "settings.admin.metricsInterval.label", + "descriptionKey": "settings.admin.metricsInterval.description", + "default": 30, + "min": 5, + "max": 3600 + }, + { + "key": "historyRetentionDays", + "type": "number", + "labelKey": "settings.admin.historyRetentionDays.label", + "descriptionKey": "settings.admin.historyRetentionDays.description", + "default": 7, + "min": 1, + "max": 90 + }, + { + "key": "enabledForNewHosts", + "type": "boolean", + "labelKey": "settings.admin.enabledForNewHosts.label", + "descriptionKey": "settings.admin.enabledForNewHosts.description", + "default": true } - ] + ], + "user": [ + { + "key": "temperatureUnit", + "type": "select", + "labelKey": "settings.user.temperatureUnit.label", + "descriptionKey": "settings.user.temperatureUnit.description", + "default": "celsius", + "options": [ + { + "value": "celsius", + "labelKey": "settings.user.temperatureUnit.celsius" + }, + { + "value": "fahrenheit", + "labelKey": "settings.user.temperatureUnit.fahrenheit" + } + ] + } + ], + "host": { + "fields": [ + { + "key": "metricsEnabled", + "type": "boolean", + "labelKey": "settings.host.metricsEnabled.label", + "default": true, + "hidden": true + }, + { + "key": "metricsInterval", + "type": "number", + "labelKey": "settings.host.metricsInterval.label", + "min": 5, + "max": 3600, + "hidden": true + }, + { + "key": "enabledWidgets", + "type": "json", + "labelKey": "settings.host.enabledWidgets.label", + "hidden": true + }, + { + "key": "excludedMounts", + "type": "json", + "labelKey": "settings.host.excludedMounts.label", + "hidden": true + }, + { + "key": "monitoredMounts", + "type": "json", + "labelKey": "settings.host.monitoredMounts.label", + "hidden": true + } + ] + } }, "actionSlots": [ { diff --git a/plugins/host-metrics/migrations/mysql/0001_adopt_host_metrics_tables.sql b/plugins/host-metrics/migrations/mysql/0001_adopt_host_metrics_tables.sql new file mode 100644 index 000000000..14067a1a6 --- /dev/null +++ b/plugins/host-metrics/migrations/mysql/0001_adopt_host_metrics_tables.sql @@ -0,0 +1,51 @@ +-- host-metrics 0001: adopt_host_metrics_tables +-- Generated by termix-plugin migrations. Review before committing. + +CREATE TABLE IF NOT EXISTS `host_metrics_preferences` ( + `id` int AUTO_INCREMENT PRIMARY KEY, + `user_id` varchar(255) NOT NULL, + `host_id` int NOT NULL, + `layout` text NOT NULL, + `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `updated_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE, + FOREIGN KEY (`host_id`) REFERENCES `ssh_data` (`id`) ON DELETE CASCADE +); +RENAME TABLE `host_metrics_preferences` TO `p_host_metrics_host_metrics_preferences`; +CREATE TABLE IF NOT EXISTS `host_health_checks` ( + `id` int AUTO_INCREMENT PRIMARY KEY, + `user_id` varchar(255) NOT NULL, + `host_id` int NOT NULL, + `checks` text NOT NULL, + `interval_seconds` int NOT NULL DEFAULT 300, + `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `updated_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE, + FOREIGN KEY (`host_id`) REFERENCES `ssh_data` (`id`) ON DELETE CASCADE +); +RENAME TABLE `host_health_checks` TO `p_host_metrics_host_health_checks`; +CREATE TABLE IF NOT EXISTS `host_health_history` ( + `id` int AUTO_INCREMENT PRIMARY KEY, + `user_id` varchar(255) NOT NULL, + `host_id` int NOT NULL, + `check_id` varchar(255) NOT NULL, + `ts` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `ok` boolean NOT NULL, + `latency_ms` int, + `detail` text, + FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE, + FOREIGN KEY (`host_id`) REFERENCES `ssh_data` (`id`) ON DELETE CASCADE +); +RENAME TABLE `host_health_history` TO `p_host_metrics_host_health_history`; +CREATE TABLE IF NOT EXISTS `host_metrics_history` ( + `id` int AUTO_INCREMENT PRIMARY KEY, + `host_id` int NOT NULL, + `ts` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `cpu_percent` double, + `mem_percent` double, + `disk_percent` double, + `net_rx_bytes` int, + `net_tx_bytes` int, + FOREIGN KEY (`host_id`) REFERENCES `ssh_data` (`id`) ON DELETE CASCADE +); +RENAME TABLE `host_metrics_history` TO `p_host_metrics_host_metrics_history`; diff --git a/plugins/host-metrics/migrations/postgres/0001_adopt_host_metrics_tables.sql b/plugins/host-metrics/migrations/postgres/0001_adopt_host_metrics_tables.sql new file mode 100644 index 000000000..5db00cd1c --- /dev/null +++ b/plugins/host-metrics/migrations/postgres/0001_adopt_host_metrics_tables.sql @@ -0,0 +1,55 @@ +-- host-metrics 0001: adopt_host_metrics_tables +-- Generated by termix-plugin migrations. Review before committing. + +CREATE TABLE IF NOT EXISTS "host_metrics_preferences" ( + "id" serial PRIMARY KEY, + "user_id" varchar(255) NOT NULL, + "host_id" integer NOT NULL, + "layout" text NOT NULL, + "created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE, + FOREIGN KEY ("host_id") REFERENCES "ssh_data" ("id") ON DELETE CASCADE +); +CREATE UNIQUE INDEX IF NOT EXISTS "idx_host_metrics_prefs_user_host" ON "host_metrics_preferences" ("user_id", "host_id"); +ALTER TABLE "host_metrics_preferences" RENAME TO "p_host_metrics_host_metrics_preferences"; +CREATE TABLE IF NOT EXISTS "host_health_checks" ( + "id" serial PRIMARY KEY, + "user_id" varchar(255) NOT NULL, + "host_id" integer NOT NULL, + "checks" text NOT NULL, + "interval_seconds" integer NOT NULL DEFAULT 300, + "created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE, + FOREIGN KEY ("host_id") REFERENCES "ssh_data" ("id") ON DELETE CASCADE +); +CREATE UNIQUE INDEX IF NOT EXISTS "idx_host_health_checks_user_host" ON "host_health_checks" ("user_id", "host_id"); +ALTER TABLE "host_health_checks" RENAME TO "p_host_metrics_host_health_checks"; +CREATE TABLE IF NOT EXISTS "host_health_history" ( + "id" serial PRIMARY KEY, + "user_id" varchar(255) NOT NULL, + "host_id" integer NOT NULL, + "check_id" varchar(255) NOT NULL, + "ts" varchar(255) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "ok" boolean NOT NULL, + "latency_ms" integer, + "detail" text, + 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_host_health_history_lookup" ON "host_health_history" ("user_id", "host_id", "check_id", "ts"); +ALTER TABLE "host_health_history" RENAME TO "p_host_metrics_host_health_history"; +CREATE TABLE IF NOT EXISTS "host_metrics_history" ( + "id" serial PRIMARY KEY, + "host_id" integer NOT NULL, + "ts" varchar(255) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "cpu_percent" double precision, + "mem_percent" double precision, + "disk_percent" double precision, + "net_rx_bytes" integer, + "net_tx_bytes" integer, + FOREIGN KEY ("host_id") REFERENCES "ssh_data" ("id") ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS "idx_host_metrics_history_host_ts" ON "host_metrics_history" ("host_id", "ts"); +ALTER TABLE "host_metrics_history" RENAME TO "p_host_metrics_host_metrics_history"; diff --git a/plugins/host-metrics/migrations/snapshot.json b/plugins/host-metrics/migrations/snapshot.json new file mode 100644 index 000000000..1fa3f380a --- /dev/null +++ b/plugins/host-metrics/migrations/snapshot.json @@ -0,0 +1,175 @@ +{ + "version": 1, + "tables": { + "host_metrics_preferences": { + "columns": { + "id": { + "type": "id", + "primaryKey": true, + "notNull": true + }, + "userId": { + "type": "refUser", + "length": 255, + "notNull": true + }, + "hostId": { + "type": "refHost", + "notNull": true + }, + "layout": { + "type": "text", + "notNull": true + }, + "createdAt": { + "type": "timestamp", + "notNull": true, + "defaultNow": true + }, + "updatedAt": { + "type": "timestamp", + "notNull": true, + "defaultNow": true + } + }, + "indexes": [ + { + "name": "idx_host_metrics_prefs_user_host", + "columns": ["userId", "hostId"], + "unique": true + } + ], + "adopts": "host_metrics_preferences" + }, + "host_health_checks": { + "columns": { + "id": { + "type": "id", + "primaryKey": true, + "notNull": true + }, + "userId": { + "type": "refUser", + "length": 255, + "notNull": true + }, + "hostId": { + "type": "refHost", + "notNull": true + }, + "checks": { + "type": "text", + "notNull": true + }, + "intervalSeconds": { + "type": "integer", + "notNull": true, + "defaultValue": 300 + }, + "createdAt": { + "type": "timestamp", + "notNull": true, + "defaultNow": true + }, + "updatedAt": { + "type": "timestamp", + "notNull": true, + "defaultNow": true + } + }, + "indexes": [ + { + "name": "idx_host_health_checks_user_host", + "columns": ["userId", "hostId"], + "unique": true + } + ], + "adopts": "host_health_checks" + }, + "host_health_history": { + "columns": { + "id": { + "type": "id", + "primaryKey": true, + "notNull": true + }, + "userId": { + "type": "refUser", + "length": 255, + "notNull": true + }, + "hostId": { + "type": "refHost", + "notNull": true + }, + "checkId": { + "type": "varchar", + "length": 255, + "notNull": true + }, + "ts": { + "type": "timestamp", + "notNull": true, + "defaultNow": true + }, + "ok": { + "type": "boolean", + "notNull": true + }, + "latencyMs": { + "type": "integer" + }, + "detail": { + "type": "text" + } + }, + "indexes": [ + { + "name": "idx_host_health_history_lookup", + "columns": ["userId", "hostId", "checkId", "ts"] + } + ], + "adopts": "host_health_history" + }, + "host_metrics_history": { + "columns": { + "id": { + "type": "id", + "primaryKey": true, + "notNull": true + }, + "hostId": { + "type": "refHost", + "notNull": true + }, + "ts": { + "type": "timestamp", + "notNull": true, + "defaultNow": true + }, + "cpuPercent": { + "type": "real" + }, + "memPercent": { + "type": "real" + }, + "diskPercent": { + "type": "real" + }, + "netRxBytes": { + "type": "integer" + }, + "netTxBytes": { + "type": "integer" + } + }, + "indexes": [ + { + "name": "idx_host_metrics_history_host_ts", + "columns": ["hostId", "ts"] + } + ], + "adopts": "host_metrics_history" + } + } +} diff --git a/plugins/host-metrics/migrations/sqlite/0001_adopt_host_metrics_tables.sql b/plugins/host-metrics/migrations/sqlite/0001_adopt_host_metrics_tables.sql new file mode 100644 index 000000000..7c015f02c --- /dev/null +++ b/plugins/host-metrics/migrations/sqlite/0001_adopt_host_metrics_tables.sql @@ -0,0 +1,55 @@ +-- host-metrics 0001: adopt_host_metrics_tables +-- Generated by termix-plugin migrations. Review before committing. + +CREATE TABLE IF NOT EXISTS "host_metrics_preferences" ( + "id" integer PRIMARY KEY AUTOINCREMENT, + "user_id" text NOT NULL, + "host_id" integer NOT NULL, + "layout" text NOT NULL, + "created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE, + FOREIGN KEY ("host_id") REFERENCES "ssh_data" ("id") ON DELETE CASCADE +); +CREATE UNIQUE INDEX IF NOT EXISTS "idx_host_metrics_prefs_user_host" ON "host_metrics_preferences" ("user_id", "host_id"); +ALTER TABLE "host_metrics_preferences" RENAME TO "p_host_metrics_host_metrics_preferences"; +CREATE TABLE IF NOT EXISTS "host_health_checks" ( + "id" integer PRIMARY KEY AUTOINCREMENT, + "user_id" text NOT NULL, + "host_id" integer NOT NULL, + "checks" text NOT NULL, + "interval_seconds" integer NOT NULL DEFAULT 300, + "created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE, + FOREIGN KEY ("host_id") REFERENCES "ssh_data" ("id") ON DELETE CASCADE +); +CREATE UNIQUE INDEX IF NOT EXISTS "idx_host_health_checks_user_host" ON "host_health_checks" ("user_id", "host_id"); +ALTER TABLE "host_health_checks" RENAME TO "p_host_metrics_host_health_checks"; +CREATE TABLE IF NOT EXISTS "host_health_history" ( + "id" integer PRIMARY KEY AUTOINCREMENT, + "user_id" text NOT NULL, + "host_id" integer NOT NULL, + "check_id" text NOT NULL, + "ts" text NOT NULL DEFAULT CURRENT_TIMESTAMP, + "ok" integer NOT NULL, + "latency_ms" integer, + "detail" text, + 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_host_health_history_lookup" ON "host_health_history" ("user_id", "host_id", "check_id", "ts"); +ALTER TABLE "host_health_history" RENAME TO "p_host_metrics_host_health_history"; +CREATE TABLE IF NOT EXISTS "host_metrics_history" ( + "id" integer PRIMARY KEY AUTOINCREMENT, + "host_id" integer NOT NULL, + "ts" text NOT NULL DEFAULT CURRENT_TIMESTAMP, + "cpu_percent" real, + "mem_percent" real, + "disk_percent" real, + "net_rx_bytes" integer, + "net_tx_bytes" integer, + FOREIGN KEY ("host_id") REFERENCES "ssh_data" ("id") ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS "idx_host_metrics_history_host_ts" ON "host_metrics_history" ("host_id", "ts"); +ALTER TABLE "host_metrics_history" RENAME TO "p_host_metrics_host_metrics_history"; diff --git a/plugins/host-metrics/src/backend/automation-bridge.ts b/plugins/host-metrics/src/backend/automation-bridge.ts deleted file mode 100644 index 37bf5417e..000000000 --- a/plugins/host-metrics/src/backend/automation-bridge.ts +++ /dev/null @@ -1,81 +0,0 @@ -/** - * One-line hand-off from the metrics poller onto the ctx.events bus. - * - * The poller is already a very large module, so the hooks it calls live here - * instead. Everything is fire-and-forget: a failure in a subscriber must never - * disturb metric collection, and publishing onto the bus creates no static edge - * to the automations layer (which reads repositories the metrics module also - * pulls in). - * - * For the generic (non-metrics) internal event notifier, see - * `hosts/automation-events.ts` -- that one is shared across features. - */ - -import { - pluginEvents, - TOPICS, -} from "../../../../src/backend/plugins/events.js"; -import type { MetricsSnapshot } from "../../../automations/src/backend/conditions.js"; - -export interface HostMetricsPayload { - hostId: number; - ownerUserId: string; - metrics: MetricsSnapshot; -} - -export interface HostStatusPayload { - hostId: number; - ownerUserId: string; - online: boolean; -} - -export interface HostHealthCheckPayload { - hostId: number; - userId: string; - checkId: string; - ok: boolean; - detail?: string; -} - -export function notifyAutomationMetrics( - hostId: number, - ownerUserId: string, - metrics: MetricsSnapshot, -): void { - if (!ownerUserId) return; - pluginEvents.emit(TOPICS.hostMetrics, { - hostId, - ownerUserId, - metrics, - } satisfies HostMetricsPayload); -} - -export function notifyAutomationStatus( - hostId: number, - ownerUserId: string, - online: boolean, -): void { - if (!ownerUserId) return; - pluginEvents.emit(TOPICS.hostStatus, { - hostId, - ownerUserId, - online, - } satisfies HostStatusPayload); -} - -export function notifyAutomationHealthCheck( - hostId: number, - userId: string, - checkId: string, - ok: boolean, - detail?: string, -): void { - if (!userId) return; - pluginEvents.emit(TOPICS.hostHealthCheck, { - hostId, - userId, - checkId, - ok, - detail, - } satisfies HostHealthCheckPayload); -} diff --git a/plugins/host-metrics/src/backend/collect.ts b/plugins/host-metrics/src/backend/collect.ts new file mode 100644 index 000000000..01f81200a --- /dev/null +++ b/plugins/host-metrics/src/backend/collect.ts @@ -0,0 +1,196 @@ +import type { Client } from "ssh2"; +import type { PluginSsh } from "@termix/plugin-sdk/backend"; +import { detectHostPlatform } from "@termix/plugin-sdk/host-commands"; +import { collectCpuMetrics } from "./widgets/cpu-collector.js"; +import { collectMemoryMetrics } from "./widgets/memory-collector.js"; +import { collectDiskMetrics } from "./widgets/disk-collector.js"; +import { collectNetworkMetrics } from "./widgets/network-collector.js"; +import { collectUptimeMetrics } from "./widgets/uptime-collector.js"; +import { collectProcessesMetrics } from "./widgets/processes-collector.js"; +import { collectSystemMetrics } from "./widgets/system-collector.js"; +import { collectLoginStats } from "./widgets/login-stats-collector.js"; +import { collectPortsMetrics } from "./widgets/ports-collector.js"; +import { collectFirewallMetrics } from "./widgets/firewall-collector.js"; +import { collectTemperatureMetrics } from "./widgets/temperature-collector.js"; +import { collectGpuMetrics, type GpuMetrics } from "./widgets/gpu-collector.js"; +import type { FirewallMetrics, PortsMetrics } from "../shared/stats-widgets.js"; +import { supportsMetrics, type MetricsHost } from "./helpers.js"; +import { sessionKey, type MetricsSessions } from "./sessions.js"; +import type { MetricsState } from "./state.js"; +import type { CollectorRegistry } from "./collectors.js"; + +export function isHostKeyVerificationError(error: unknown): boolean { + return ( + error instanceof Error && + (error.message.includes("Host denied (verification failed)") || + error.message.includes("Host key changed")) + ); +} + +export type CollectedMetrics = { + cpu: Awaited>; + memory: Awaited>; + disk: Awaited>; + network: Awaited>; + uptime: Awaited>; + processes: Awaited>; + system: Awaited>; + login_stats: Awaited>; + ports: PortsMetrics; + firewall: FirewallMetrics; + temperature: Awaited>; + gpu: GpuMetrics; + /** Results from other plugins' collectors, by collector id. */ + extra?: Record; +}; + +export interface CollectDeps { + ssh: PluginSsh; + state: MetricsState; + sessions: MetricsSessions; + collectors: CollectorRegistry; +} + +async function optional(collect: () => Promise, fallback: T) { + try { + return await collect(); + } catch { + return fallback; + } +} + +/** + * One sample. Reuses a connection the person opened from the tab when there + * is one, otherwise borrows the "stats" pool, which answers password prompts + * only and fails fast on anything that needs a person. + */ +export async function collectMetrics( + deps: CollectDeps, + host: MetricsHost, + mounts: { + excludedMounts: string[]; + monitoredMounts: Array<{ path: string; label?: string }>; + }, + onAuthenticated?: () => void, +): Promise { + const { ssh, state, sessions, collectors } = deps; + if (!supportsMetrics(host, ssh)) { + throw new Error("Metrics collection only supported for SSH hosts"); + } + + if (state.authFailures.shouldSkip(host.id)) { + const reason = state.authFailures.getSkipReason(host.id); + throw new Error(reason || "Authentication failed"); + } + + const cached = state.metricsCache.get(host.id) as CollectedMetrics | null; + if (cached) { + onAuthenticated?.(); + return cached; + } + + return state.requestQueue.queueRequest(host.id, async () => { + const key = sessionKey(host.id, host.userId); + const existing = sessions.get(key); + + try { + const collect = async (client: Client): Promise => { + onAuthenticated?.(); + const platform = await detectHostPlatform(client); + const result: CollectedMetrics = { + cpu: await collectCpuMetrics(client, platform, host.id), + memory: await collectMemoryMetrics(client, platform), + disk: await collectDiskMetrics( + client, + mounts.excludedMounts, + mounts.monitoredMounts, + platform, + ), + network: await collectNetworkMetrics(client, platform, host.id), + uptime: await collectUptimeMetrics(client, platform), + processes: await collectProcessesMetrics(client), + system: await collectSystemMetrics(client, platform), + login_stats: await optional(() => collectLoginStats(client), { + recentLogins: [], + failedLogins: [], + totalLogins: 0, + uniqueIPs: 0, + }), + ports: await optional(() => collectPortsMetrics(client), { + source: "none", + ports: [], + }), + firewall: await optional(() => collectFirewallMetrics(client), { + type: "none", + status: "unknown", + chains: [], + }), + temperature: await optional(() => collectTemperatureMetrics(client), { + source: "none", + highestCelsius: null, + sensors: [], + }), + gpu: await optional(() => collectGpuMetrics(client), { + source: "none", + gpus: [], + processes: [], + }), + }; + + const extra = await collectors.run(client, host.id, host.userId); + if (Object.keys(extra).length > 0) result.extra = extra; + + state.metricsCache.set(host.id, result); + return result; + }; + + if (existing?.isConnected) { + existing.activeOperations++; + try { + const result = await collect(existing.client); + sessions.touch(key); + return result; + } finally { + existing.activeOperations--; + } + } + + return await ssh.withConnection( + host, + { + pool: "stats", + purpose: "metrics", + overrides: { readyTimeout: 60000 }, + }, + collect, + ); + } catch (error) { + if (error instanceof Error) { + if (error.message.includes("TOTP authentication required")) { + throw error; + } else if ( + error.message.includes("No password available") || + error.message.includes("Unsupported authentication type") || + error.message.includes("No SSH key available") || + error.message.includes("Invalid SSH key format") + ) { + state.authFailures.recordFailure(host.id, "AUTH", true); + } else if (isHostKeyVerificationError(error)) { + state.authFailures.recordFailure(host.id, "HOST_KEY", true); + } else if ( + error.message.includes("authentication") || + error.message.includes("Permission denied") || + error.message.includes("All configured authentication methods failed") + ) { + state.authFailures.recordFailure(host.id, "AUTH"); + } else if ( + error.message.includes("timeout") || + error.message.includes("ETIMEDOUT") + ) { + state.authFailures.recordFailure(host.id, "TIMEOUT"); + } + } + throw error; + } + }); +} diff --git a/plugins/host-metrics/src/backend/collectors.ts b/plugins/host-metrics/src/backend/collectors.ts new file mode 100644 index 000000000..148c3c609 --- /dev/null +++ b/plugins/host-metrics/src/backend/collectors.ts @@ -0,0 +1,48 @@ +import type { Client } from "ssh2"; +import type { PluginServices } from "@termix/plugin-sdk/backend"; +import type { MetricsLogger } from "./log.js"; + +export const COLLECTORS_SERVICE = "host-metrics.collectors"; + +/** + * The "host-metrics.collectors" service, version 1. Another plugin provides + * it under its own provider name (manifest `provides[].names`) to add data + * to every sample. It runs on the sample's SSH connection, as the host's + * owner, and its result lands in the snapshot under `extra[]`. A + * collector that throws is skipped for that sample. + */ +export interface MetricsCollectorV1 { + collect: (client: Client, hostId: number) => Promise; +} + +export class CollectorRegistry { + constructor( + private readonly services: PluginServices, + private readonly log: MetricsLogger, + ) {} + + async run( + client: Client, + hostId: number, + ownerUserId: string, + ): Promise> { + const results: Record = {}; + for (const provider of this.services.providers(COLLECTORS_SERVICE)) { + if (!provider) continue; + try { + const collector = this.services.get>( + COLLECTORS_SERVICE, + { provider, userId: ownerUserId }, + ); + if (typeof collector.collect !== "function") continue; + results[provider] = await collector.collect(client, hostId); + } catch (error) { + this.log.debug(`Metrics collector ${provider} failed`, { + hostId, + error: error instanceof Error ? error.message : String(error), + }); + } + } + return results; + } +} diff --git a/plugins/host-metrics/src/backend/helpers.ts b/plugins/host-metrics/src/backend/helpers.ts index d6020bed8..7681aab6b 100644 --- a/plugins/host-metrics/src/backend/helpers.ts +++ b/plugins/host-metrics/src/backend/helpers.ts @@ -1,60 +1,51 @@ -import type { Client } from "ssh2"; -import { pluginSsh } from "./ssh.js"; +import type { PluginSsh, PluginSshHost } from "@termix/plugin-sdk/backend"; -export type StatsCapableHost = { - connectionType?: string; - authType?: string; +/** A host as ctx.ssh.resolveHost returns it, with the fields polling reads. */ +export type MetricsHost = PluginSshHost & { + userId: string; + name?: string | null; + connectionType?: string | null; + enableSsh?: boolean | null; + password?: string | null; + sudoPassword?: string | null; + terminalConfig?: { sudoPassword?: string | null } | null; + enableDocker?: boolean | null; + jumpHosts?: Array<{ hostId: number }> | null; }; -export type TcpPingStatsConfig = { - statusCheckEnabled: boolean; - disableTcpPing?: boolean; -}; - -export function supportsMetrics(host: StatsCapableHost): boolean { +/** Metrics run over SSH, unattended, so the auth type has to allow that. */ +export function supportsMetrics( + host: Pick, + ssh: Pick, +): boolean { const connectionType = host.connectionType || "ssh"; - if (connectionType !== "ssh") return false; - return pluginSsh().supportsBackground(host.authType || "none"); + if (connectionType !== "ssh" && host.enableSsh !== true) return false; + return ssh.supportsBackground(host.authType || "none"); } -export function isTcpPingEnabled(statsConfig: TcpPingStatsConfig): boolean { - return statsConfig.statusCheckEnabled && !statsConfig.disableTcpPing; +export function sudoPasswordOf(host: MetricsHost): string | undefined { + return host.sudoPassword || host.terminalConfig?.sudoPassword || undefined; } -export function parseStatusHostIds(value: unknown): Set | null { - if (value === undefined) return null; - if (typeof value !== "string") return new Set(); +export type ConnectionLogType = "info" | "success" | "warning" | "error"; - return new Set( - value - .split(",") - .map(Number) - .filter((id) => Number.isSafeInteger(id) && id > 0), - ); +export interface ConnectionLog { + type: ConnectionLogType; + stage: string; + message: string; + details?: Record; } -export function tcpPingThroughJumpHost( - jumpClient: Pick, - host: string, - port: number, - timeoutMs = 5000, -): Promise { - return new Promise((resolve) => { - let settled = false; +/** The entry shape the connection log panel reads. */ +export function connectionLog( + type: ConnectionLogType, + stage: string, + message: string, + details?: Record, +): ConnectionLog { + return { type, stage, message, ...(details ? { details } : {}) }; +} - const finish = (result: boolean) => { - if (settled) return; - settled = true; - clearTimeout(timeout); - jumpClient.end(); - resolve(result); - }; - - const timeout = setTimeout(() => finish(false), timeoutMs); - - jumpClient.forwardOut("127.0.0.1", 0, host, port, (error, stream) => { - stream?.destroy(); - finish(!error && !!stream); - }); - }); +export function newSessionId(prefix: string): string { + return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`; } diff --git a/plugins/host-metrics/src/backend/history-routes.ts b/plugins/host-metrics/src/backend/history-routes.ts deleted file mode 100644 index f832e81f3..000000000 --- a/plugins/host-metrics/src/backend/history-routes.ts +++ /dev/null @@ -1,131 +0,0 @@ -import type { Express, RequestHandler } from "express"; -import type { AuthenticatedRequest } from "../../../../src/types/index.js"; -import { createCurrentHostMetricsHistoryRepository } from "../../../../src/backend/database/repositories/factory.js"; -import { statsLogger } from "../../../../src/backend/utils/logger.js"; -import type { HostAction } from "../../../../src/backend/utils/permission-manager.js"; - -type HistoryRoutesDeps = { - validateHostId: RequestHandler; - canAccessHost: ( - userId: string, - hostId: number, - level: HostAction, - ) => Promise; -}; - -const RANGE_OFFSETS: Record = { - "1h": 1 * 60 * 60 * 1000, - "6h": 6 * 60 * 60 * 1000, - "24h": 24 * 60 * 60 * 1000, - "7d": 7 * 24 * 60 * 60 * 1000, - "30d": 30 * 24 * 60 * 60 * 1000, -}; - -export function registerHostMetricsHistoryRoutes( - app: Express, - { validateHostId, canAccessHost }: HistoryRoutesDeps, -): void { - /** - * @openapi - * /metrics/history/{id}: - * get: - * summary: Get historical metrics for a host - * tags: - * - Host Metrics - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: integer - * - in: query - * name: range - * schema: - * type: string - * enum: [1h, 6h, 24h, 7d, 30d] - * - in: query - * name: from - * schema: - * type: string - * format: date-time - * - in: query - * name: to - * schema: - * type: string - * format: date-time - * responses: - * 200: - * description: Array of metric history rows. - * 403: - * description: Access denied. - * 404: - * description: Host not found or no access. - */ - app.get("/metrics/history/:id", validateHostId, async (req, res) => { - const hostId = Number(req.params.id); - const userId = (req as AuthenticatedRequest).userId; - - try { - const hasAccess = await canAccessHost(userId, hostId, "connect"); - if (!hasAccess) { - return res.status(403).json({ error: "Access denied" }); - } - - const { range, from, to } = req.query as Record< - string, - string | undefined - >; - - let fromTs: string; - let toTs: string = new Date().toISOString(); - - if (range) { - const offsetMs = RANGE_OFFSETS[range]; - if (!offsetMs) { - return res - .status(400) - .json({ error: "Invalid range. Use 1h, 6h, 24h, 7d, or 30d" }); - } - fromTs = new Date(Date.now() - offsetMs).toISOString(); - } else if (from && to) { - const fromDate = new Date(from); - const toDate = new Date(to); - if (isNaN(fromDate.getTime()) || isNaN(toDate.getTime())) { - return res.status(400).json({ error: "Invalid from/to date format" }); - } - fromTs = fromDate.toISOString(); - toTs = toDate.toISOString(); - } else { - fromTs = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); - } - - // SQLite CURRENT_TIMESTAMP stores 'YYYY-MM-DD HH:MM:SS' (no T/Z). - // Normalize both sides to that format for reliable string comparison. - const toSqlite = (iso: string) => - iso.replace("T", " ").replace(/\.\d{3}Z$/, ""); - const rows = ( - await createCurrentHostMetricsHistoryRepository().listRange( - hostId, - toSqlite(fromTs), - toSqlite(toTs), - ) - ).map((row) => ({ - ts: row.ts, - cpu_percent: row.cpuPercent, - mem_percent: row.memPercent, - disk_percent: row.diskPercent, - net_rx_bytes: row.netRxBytes, - net_tx_bytes: row.netTxBytes, - })); - - res.json({ rows, fromTs, toTs }); - } catch (error) { - statsLogger.error("Failed to fetch metrics history", { - operation: "metrics_history_fetch_error", - hostId, - error: error instanceof Error ? error.message : String(error), - }); - res.status(500).json({ error: "Failed to fetch metrics history" }); - } - }); -} diff --git a/plugins/host-metrics/src/backend/host-import.ts b/plugins/host-metrics/src/backend/host-import.ts new file mode 100644 index 000000000..025f90703 --- /dev/null +++ b/plugins/host-metrics/src/backend/host-import.ts @@ -0,0 +1,32 @@ +import { readHostMetricsSettings } from "../shared/stats-widgets.js"; + +/** + * Registered as ctx.registry.provide("host-metrics.hostImportNormalizer", ...) + * for core's bulk host import. A Termix export carries these settings under + * pluginSettings["host-metrics"]; exports from before 2.9.0 carried them in + * statsConfig, where the host's own interval only counted when + * useGlobalMetricsInterval was false. + */ +export function hostImportNormalizer( + raw: Record, +): Record | null { + const bag = raw.pluginSettings as + Record> | undefined; + const own = bag?.["host-metrics"]; + if (own && typeof own === "object") { + const settings = readHostMetricsSettings(own); + return { ...settings }; + } + + const legacy = + raw.statsConfig && typeof raw.statsConfig === "object" + ? (raw.statsConfig as Record) + : null; + if (!legacy) return null; + const settings = readHostMetricsSettings({ + ...legacy, + metricsInterval: + legacy.useGlobalMetricsInterval === false ? legacy.metricsInterval : null, + }); + return { ...settings }; +} diff --git a/plugins/host-metrics/src/backend/index.ts b/plugins/host-metrics/src/backend/index.ts index c177a3481..aa07de5fe 100644 --- a/plugins/host-metrics/src/backend/index.ts +++ b/plugins/host-metrics/src/backend/index.ts @@ -1,34 +1,108 @@ +import type { Router } from "express"; import type { PluginContext } from "@termix/plugin-sdk/backend"; -import { setPluginSsh } from "./ssh.js"; -import { - onHostDeleted, - onHostLogin, - onHostUpdated, - shutdown, - startHostMetricsService, -} from "./routes.js"; +import { createLogger } from "./log.js"; +import { createHostMetricsRepository } from "./repository.js"; +import { createMetricsState } from "./state.js"; +import { MetricsSessions } from "./sessions.js"; +import { CollectorRegistry } from "./collectors.js"; +import { MetricsPoller, TOPIC_HEALTH_CHECK } from "./poller.js"; +import { registerRoutes } from "./routes.js"; +import { hostImportNormalizer } from "./host-import.js"; +import { newSessionId, supportsMetrics } from "./helpers.js"; + +export type { MetricsCollectorV1 } from "./collectors.js"; +export type { CollectionStatusPayload, SnapshotPayload } from "./poller.js"; + +/** The "host-metrics.viewers" service, version 1. Runs as the caller's user. */ +export interface MetricsViewersV1 { + register: ( + hostId: number, + ) => Promise<{ viewerSessionId: string } | { skipped: true; reason: string }>; + heartbeat: (viewerSessionId: string) => boolean; + unregister: (hostId: number, viewerSessionId: string) => void; +} export async function activate(ctx: PluginContext) { - setPluginSsh(ctx.ssh); - ctx.disposables.add(() => setPluginSsh(null)); - await startHostMetricsService(ctx.http.router()); + const log = createLogger(ctx.log); + const repository = await createHostMetricsRepository(ctx.db); + const state = createMetricsState(); + const sessions = new MetricsSessions(ctx.schedule, log); + const collectors = new CollectorRegistry(ctx.services, log); + const poller = new MetricsPoller({ + ctx, + log, + repository, + ssh: ctx.ssh, + state, + sessions, + collectors, + }); + ctx.disposables.add(() => { + poller.dispose(); + sessions.dispose(); + }); + + registerRoutes(ctx.http.router(), { + ctx, + log, + poller, + sessions, + repository, + onHealthCheck: (event) => ctx.events.emit(TOPIC_HEALTH_CHECK, event), + }); + + ctx.schedule.every(60_000, () => poller.cleanupInactiveViewers()); + ctx.schedule.every(10 * 60_000, () => { + state.authFailures.cleanup(); + state.backoff.cleanup(); + }); - // Core used to POST these to this plugin's own port with an internal auth - // token. They are events now: no port, no shared secret, and the - // subscriptions are torn down with the plugin. ctx.events.on("host.updated", (payload) => { - void onHostUpdated(payload as Parameters[0]); + const { hostId } = payload as { hostId?: number }; + if (hostId) void poller.hostUpdated(hostId); }); ctx.events.on("host.deleted", (payload) => { - onHostDeleted(payload as Parameters[0]); + const { hostId } = payload as { hostId?: number }; + if (hostId) poller.hostDeleted(hostId); }); - ctx.events.on("host.login", (payload) => { - onHostLogin(payload as Parameters[0]); + ctx.events.on("host.status", (payload) => { + const { hostId, status } = payload as { hostId?: number; status?: string }; + if (hostId && status) poller.hostStatusChanged(hostId, status); }); + ctx.events.on("host.key.updated", (payload) => { + const { hostId } = payload as { hostId?: number }; + if (hostId) poller.hostKeyAccepted(hostId); + }); + ctx.settings.onChange("metricsInterval", () => void poller.retimeAll()); + + ctx.services.provide("host-metrics.viewers", { + register: async (hostId) => { + const userId = ctx.currentActor(); + if (!userId) return { skipped: true, reason: "no_user" }; + const host = await poller.resolve(hostId, userId); + if (!host) return { skipped: true, reason: "host_not_found" }; + if (!supportsMetrics(host, ctx.ssh)) { + return { skipped: true, reason: "metrics_unsupported" }; + } + if (!(await poller.settingsFor(hostId)).metricsEnabled) { + return { skipped: true, reason: "metrics_disabled" }; + } + const viewerSessionId = newSessionId("service"); + poller.registerViewer(hostId, viewerSessionId, userId); + return { viewerSessionId }; + }, + heartbeat: (viewerSessionId) => + poller.updateHeartbeat(viewerSessionId, ctx.currentActor()), + unregister: (hostId, viewerSessionId) => + poller.unregisterViewer(hostId, viewerSessionId, ctx.currentActor()), + }); + + ctx.registry.provide( + "host-metrics.hostImportNormalizer", + hostImportNormalizer, + ); ctx.log.info("Host Metrics mounted at /plugin-api/host-metrics"); } -export async function deactivate() { - shutdown(); -} +export async function deactivate() {} diff --git a/plugins/host-metrics/src/backend/interactive.ts b/plugins/host-metrics/src/backend/interactive.ts new file mode 100644 index 000000000..f4571496b --- /dev/null +++ b/plugins/host-metrics/src/backend/interactive.ts @@ -0,0 +1,367 @@ +import { Client, type ConnectConfig } from "ssh2"; +import type { PluginSsh } from "@termix/plugin-sdk/backend"; +import { + connectionLog, + newSessionId, + type ConnectionLog, + type MetricsHost, +} from "./helpers.js"; +import type { MetricsLogger } from "./log.js"; +import { sessionKey, type MetricsSessions } from "./sessions.js"; + +const CONNECT_TIMEOUT_MS = 60_000; +const TOTP_TTL_MS = 180_000; +const MAX_TOTP_ATTEMPTS = 3; + +export type StartResult = + | { status: 200; body: Record } + | { status: 400 | 401 | 500; body: Record }; + +export interface InteractiveDeps { + ssh: PluginSsh; + sessions: MetricsSessions; + log: MetricsLogger; + registerViewer: (hostId: number, sessionId: string, userId: string) => void; +} + +function stageForError(message: string): [string, string] { + if (message.includes("ENOTFOUND") || message.includes("getaddrinfo")) { + return ["dns", `DNS resolution failed: ${message}`]; + } + if (message.includes("ECONNREFUSED") || message.includes("ETIMEDOUT")) { + return ["tcp", `TCP connection failed: ${message}`]; + } + if (message.includes("handshake") || message.includes("key exchange")) { + return ["handshake", `SSH handshake failed: ${message}`]; + } + if (/authentication/i.test(message)) { + return ["auth", `Authentication failed: ${message}`]; + } + if (message.includes("verification failed")) { + return [ + "handshake", + "SSH host key has changed. For security, please open a Terminal connection to this host first to verify and accept the new key fingerprint.", + ]; + } + return ["error", `SSH connection failed: ${message}`]; +} + +/** + * Opens a connection from the tab, where a person can answer a TOTP prompt, + * and keeps it for polling. Resolves with the HTTP answer. + */ +export async function startInteractive( + deps: InteractiveDeps, + host: MetricsHost, + userId: string, +): Promise { + const { ssh, sessions, log } = deps; + const logs: ConnectionLog[] = [ + connectionLog("info", "stats_connecting", "Starting metrics collection"), + connectionLog("info", "dns", `Resolving DNS for ${host.ip}`), + connectionLog("info", "tcp", `Connecting to ${host.ip}:${host.port}`), + connectionLog("info", "handshake", "Initiating SSH handshake"), + ]; + if (host.authType === "password") { + logs.push(connectionLog("info", "auth", "Authenticating with password")); + } else if (host.authType === "key") { + logs.push(connectionLog("info", "auth", "Authenticating with SSH key")); + } + + const key = sessionKey(host.id, userId); + if (sessions.get(key)?.isConnected) { + logs.push( + connectionLog( + "success", + "stats_polling", + "Using existing metrics session", + ), + ); + return { status: 200, body: { success: true, connectionLogs: logs } }; + } + + const client = new Client(); + const prepared = await ssh.prepare(host, { + purpose: "metrics", + client, + log: (level, message) => logs.push(connectionLog(level, "auth", message)), + }); + const config = prepared.config as ConnectConfig; + config.readyTimeout = CONNECT_TIMEOUT_MS; + if (prepared.outcome.status !== "ready") { + const outcome = prepared.outcome; + logs.push(connectionLog("error", "auth", outcome.message)); + if (outcome.status === "interaction-required") { + return { + status: 401, + body: { + error: outcome.message, + requiresAuthInteraction: outcome.interaction, + ...(outcome.flag ? { [outcome.flag]: true } : {}), + connectionLogs: logs, + }, + }; + } + return { + status: 400, + body: { error: outcome.message, connectionLogs: logs }, + }; + } + + try { + const result = await new Promise>( + (resolve, reject) => { + let settled = false; + const settle = (fn: () => void) => { + if (settled) return; + settled = true; + clearTimeout(timeout); + fn(); + }; + const timeout = setTimeout( + () => + settle(() => { + client.end(); + reject(new Error("Connection timeout")); + }), + CONNECT_TIMEOUT_MS, + ); + + client.on( + "keyboard-interactive", + (name, instructions, _lang, prompts, finish) => { + const decision = ssh.classifyKeyboardInteractive( + { name, instructions, prompts }, + host, + ); + if (decision.kind !== "totp") { + finish(ssh.autoResponses(prompts, host.password)); + return; + } + const sessionId = newSessionId("totp"); + sessions.addPending({ + client, + finish, + config, + createdAt: Date.now(), + sessionId, + hostId: host.id, + userId, + prompts: prompts.map((p) => ({ + prompt: p.prompt, + echo: p.echo ?? false, + })), + totpPromptIndex: decision.promptIndex, + resolvedPassword: host.password ?? undefined, + totpAttempts: 0, + }); + logs.push( + connectionLog("info", "stats_totp", "TOTP verification required"), + ); + settle(() => + resolve({ + success: false, + requires_totp: true, + sessionId, + prompt: prompts[decision.promptIndex].prompt, + }), + ); + }, + ); + + client.on("ready", () => + settle(() => { + logs.push( + connectionLog( + "success", + "connected", + "SSH connection established successfully", + ), + connectionLog( + "success", + "stats_polling", + "Metrics session established", + ), + ); + sessions.open(key, { + client, + isConnected: true, + lastActive: Date.now(), + activeOperations: 0, + hostId: host.id, + userId, + }); + const viewerSessionId = newSessionId("viewer"); + deps.registerViewer(host.id, viewerSessionId, userId); + resolve({ success: true, viewerSessionId }); + }), + ); + + client.on("error", (error) => + settle(() => { + const message = + error instanceof Error ? error.message : String(error); + const [stage, text] = stageForError(message); + logs.push(connectionLog("error", stage, text)); + log.error("SSH connection error in metrics/start", { + operation: "metrics_start_ssh_error", + hostId: host.id, + error: message, + }); + reject(error); + }), + ); + + if (Array.isArray(host.jumpHosts) && host.jumpHosts.length > 0) { + logs.push( + connectionLog("info", "proxy", "Connecting via jump host chain"), + ); + } + ssh + .openTransport(host, config as Record) + .then((transport) => { + if (transport.jumpClient) { + const jumpClient = transport.jumpClient as Client; + client.on("close", () => jumpClient.end()); + } + client.connect(config); + }) + .catch((error) => + settle(() => { + logs.push( + connectionLog( + "error", + "proxy", + `Connection setup failed: ${error instanceof Error ? error.message : String(error)}`, + ), + ); + reject(error); + }), + ); + }, + ); + return { status: 200, body: { ...result, connectionLogs: logs } }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + logs.push( + connectionLog( + "error", + "stats_connecting", + `Failed to start metrics: ${message}`, + ), + ); + return { + status: 500, + body: { + error: message || "Failed to start metrics collection", + connectionLogs: logs, + }, + }; + } +} + +export type TotpResult = { status: number; body: Record }; + +/** Sends the TOTP code a person typed and keeps the connection on success. */ +export async function submitTotp( + deps: InteractiveDeps, + userId: string, + sessionId: string, + totpCode: string, +): Promise { + const { sessions, log } = deps; + const session = sessions.getPending(sessionId); + if (!session) { + return { + status: 404, + body: { error: "TOTP session not found or expired" }, + }; + } + if (Date.now() - session.createdAt > TOTP_TTL_MS) { + sessions.dropPending(sessionId, true); + return { status: 408, body: { error: "TOTP session timeout" } }; + } + if (session.userId !== userId) { + return { status: 403, body: { error: "Unauthorized" } }; + } + + session.totpAttempts++; + if (session.totpAttempts > MAX_TOTP_ATTEMPTS) { + sessions.dropPending(sessionId, true); + return { status: 429, body: { error: "Too many TOTP attempts" } }; + } + + const passwordFor = (prompt: string) => + /password/i.test(prompt) && session.resolvedPassword + ? session.resolvedPassword + : ""; + + try { + const responses = (session.prompts || []).map((p, index) => + index === session.totpPromptIndex + ? totpCode.trim() + : passwordFor(p.prompt), + ); + + const ready = new Promise((resolve, reject) => { + const timeout = setTimeout( + () => reject(new Error("TOTP verification timeout")), + 30_000, + ); + session.client.once( + "keyboard-interactive", + (_name, _instructions, _lang, prompts, finish) => { + log.warn("Second keyboard-interactive received after TOTP", { + operation: "totp_second_keyboard_interactive", + hostId: session.hostId, + }); + finish(prompts.map((p) => passwordFor(p.prompt))); + }, + ); + session.client.once("ready", () => { + clearTimeout(timeout); + resolve(); + }); + session.client.once("error", (error) => { + clearTimeout(timeout); + reject(error); + }); + }); + + session.finish(responses); + await ready; + + sessions.open(sessionKey(session.hostId, userId), { + client: session.client, + isConnected: true, + lastActive: Date.now(), + activeOperations: 0, + hostId: session.hostId, + userId, + }); + sessions.dropPending(sessionId); + + const viewerSessionId = newSessionId("viewer"); + deps.registerViewer(session.hostId, viewerSessionId, userId); + return { status: 200, body: { success: true, viewerSessionId } }; + } catch (error) { + log.error("TOTP verification failed", { + operation: "totp_verification_failed", + hostId: session.hostId, + error: error instanceof Error ? error.message : String(error), + }); + if (session.totpAttempts >= MAX_TOTP_ATTEMPTS) { + sessions.dropPending(sessionId, true); + } + return { + status: 401, + body: { + error: "TOTP verification failed", + attemptsRemaining: Math.max( + 0, + MAX_TOTP_ATTEMPTS - session.totpAttempts, + ), + }, + }; + } +} diff --git a/plugins/host-metrics/src/backend/log.ts b/plugins/host-metrics/src/backend/log.ts new file mode 100644 index 000000000..e8ce18356 --- /dev/null +++ b/plugins/host-metrics/src/backend/log.ts @@ -0,0 +1,38 @@ +import type { PluginLogger } from "@termix/plugin-sdk/backend"; + +export type Meta = Record; + +export interface MetricsLogger { + debug: (message: string, meta?: Meta) => void; + info: (message: string, meta?: Meta) => void; + warn: (message: string, meta?: Meta) => void; + /** An error, or structured context, or both. */ + error: (message: string, errorOrMeta?: unknown, meta?: Meta) => void; +} + +function withMeta(message: string, meta?: Meta): string { + return meta && Object.keys(meta).length > 0 + ? `${message} ${JSON.stringify(meta)}` + : message; +} + +/** ctx.log with the structured context the old stats logger carried. */ +export function createLogger(log: PluginLogger): MetricsLogger { + return { + debug: (message, meta) => log.debug(withMeta(message, meta)), + info: (message, meta) => log.info(withMeta(message, meta)), + warn: (message, meta) => log.warn(withMeta(message, meta)), + error: (message, errorOrMeta, meta) => { + if (errorOrMeta instanceof Error) { + log.error(withMeta(message, meta), errorOrMeta); + return; + } + log.error( + withMeta(message, { + ...((errorOrMeta as Meta | undefined) ?? {}), + ...meta, + }), + ); + }, + }; +} diff --git a/plugins/host-metrics/src/backend/managers/cron.ts b/plugins/host-metrics/src/backend/managers/cron.ts index 501c294aa..393f3e585 100644 --- a/plugins/host-metrics/src/backend/managers/cron.ts +++ b/plugins/host-metrics/src/backend/managers/cron.ts @@ -1,7 +1,9 @@ -import type { Express } from "express"; -import { execCommand } from "../../../../../src/backend/hosts/metrics-shared/common-utils.js"; +import { + execCommand, + shellSingleQuote, +} from "@termix/plugin-sdk/host-commands"; +import type { Router } from "express"; import { managerHandler, ManagerInputError } from "./route-helpers.js"; -import { shellSingleQuote } from "../../../../../src/backend/hosts/metrics-shared/exec-elevated.js"; import type { ManagerRoutesDeps } from "./types.js"; export interface CronEntry { @@ -78,14 +80,12 @@ export function buildApplyCrontabCommand(body: string): string { return `printf '%s' ${shellSingleQuote(body)} | crontab -`; } -export function registerCronRoutes( - app: Express, - { validateHostId, runOnHost }: ManagerRoutesDeps, -): void { +export function registerCronRoutes(app: Router, deps: ManagerRoutesDeps): void { + const { validateHostId } = deps; app.get( "/host-metrics/managers/cron/:id", validateHostId, - managerHandler(runOnHost, "connect", "cron_list", async (client) => { + managerHandler(deps, "connect", "cron_list", async (client) => { const { stdout } = await execCommand(client, READ_CRONTAB_CMD, 15000); return { entries: parseCrontab(stdout) }; }), @@ -95,7 +95,7 @@ export function registerCronRoutes( "/host-metrics/managers/cron/:id", validateHostId, managerHandler( - runOnHost, + deps, "connect", "cron_replace", async (client, _host, req) => { diff --git a/plugins/host-metrics/src/backend/managers/firewall.ts b/plugins/host-metrics/src/backend/managers/firewall.ts index 76f0bc92f..3723b1bf6 100644 --- a/plugins/host-metrics/src/backend/managers/firewall.ts +++ b/plugins/host-metrics/src/backend/managers/firewall.ts @@ -1,15 +1,14 @@ -import type { Express } from "express"; -import { collectFirewallMetrics } from "../widgets/firewall-collector.js"; -import { execElevated } from "../../../../../src/backend/hosts/metrics-shared/exec-elevated.js"; -import { managerHandler, ManagerInputError } from "./route-helpers.js"; +import { execElevated, detectPlatform } from "@termix/plugin-sdk/host-commands"; import { isValidPort, isValidIpProtocol, isValidFirewallTarget, type IpProtocol, type FirewallTarget, -} from "../../../../../src/backend/hosts/metrics-shared/validation.js"; -import { detectPlatform } from "../../../../../src/backend/hosts/metrics-shared/platform.js"; +} from "./validation.js"; +import type { Router } from "express"; +import { collectFirewallMetrics } from "../widgets/firewall-collector.js"; +import { managerHandler, ManagerInputError } from "./route-helpers.js"; import type { ManagerRoutesDeps } from "./types.js"; export interface FirewallRuleSpec { @@ -45,13 +44,14 @@ export function buildNftRuleCommand( } export function registerFirewallRoutes( - app: Express, - { validateHostId, runOnHost }: ManagerRoutesDeps, + app: Router, + deps: ManagerRoutesDeps, ): void { + const { validateHostId } = deps; app.get( "/host-metrics/managers/firewall/:id", validateHostId, - managerHandler(runOnHost, "connect", "firewall_read", async (client) => { + managerHandler(deps, "connect", "firewall_read", async (client) => { return await collectFirewallMetrics(client); }), ); @@ -60,7 +60,7 @@ export function registerFirewallRoutes( "/host-metrics/managers/firewall/:id/rule", validateHostId, managerHandler( - runOnHost, + deps, "connect", "firewall_rule", async (client, host, req) => { @@ -105,7 +105,7 @@ export function registerFirewallRoutes( "/host-metrics/managers/firewall/:id/persist", validateHostId, managerHandler( - runOnHost, + deps, "connect", "firewall_persist", async (client, host) => { diff --git a/plugins/host-metrics/src/backend/managers/health.ts b/plugins/host-metrics/src/backend/managers/health.ts index 457eef4e6..c9eec0dc7 100644 --- a/plugins/host-metrics/src/backend/managers/health.ts +++ b/plugins/host-metrics/src/backend/managers/health.ts @@ -1,13 +1,13 @@ -import type { Express } from "express"; +import { + execCommand, + shellSingleQuote, +} from "@termix/plugin-sdk/host-commands"; +import { isValidPort } from "./validation.js"; +import type { Router } from "express"; import type { Client } from "ssh2"; -import type { AuthenticatedRequest } from "../../../../../src/types/index.js"; -import { execCommand } from "../../../../../src/backend/hosts/metrics-shared/common-utils.js"; -import { createCurrentHostHealthRepository } from "../../../../../src/backend/database/repositories/factory.js"; import { managerHandler, ManagerInputError } from "./route-helpers.js"; -import { shellSingleQuote } from "../../../../../src/backend/hosts/metrics-shared/exec-elevated.js"; -import { isValidPort } from "../../../../../src/backend/hosts/metrics-shared/validation.js"; import type { ManagerRoutesDeps } from "./types.js"; -import { notifyAutomationHealthCheck } from "../automation-bridge.js"; +import type { HostMetricsRepository } from "../repository.js"; export interface HealthCheck { id: string; @@ -111,24 +111,12 @@ async function runChecks( const HISTORY_KEEP = 500; -function recordHistory( - userId: string, - hostId: number, - results: HealthResult[], -): Promise { - return createCurrentHostHealthRepository() - .recordHistory(userId, hostId, results, HISTORY_KEEP) - .then(() => undefined); -} - async function loadChecks( + repository: HostMetricsRepository, userId: string, hostId: number, ): Promise { - const row = await createCurrentHostHealthRepository().findChecksByUserAndHost( - userId, - hostId, - ); + const row = await repository.findChecks(userId, hostId); if (!row?.checks) return []; try { const parsed = JSON.parse(row.checks); @@ -139,60 +127,50 @@ async function loadChecks( } export function registerHealthRoutes( - app: Express, - { validateHostId, runOnHost }: ManagerRoutesDeps, + app: Router, + deps: ManagerRoutesDeps, ): void { + const { validateHostId, repository } = deps; + + const record = async ( + userId: string, + hostId: number, + results: HealthResult[], + ) => { + if (!results.length) return; + await repository.recordHealth(userId, hostId, results, HISTORY_KEEP); + for (const result of results) { + deps.onHealthCheck({ + hostId, + userId, + checkId: result.checkId, + ok: result.ok, + detail: result.detail ?? undefined, + }); + } + }; app.get( "/host-metrics/managers/health/:id", validateHostId, - managerHandler( - runOnHost, - "connect", - "health_get", - async (client, host, req) => { - const userId = (req as AuthenticatedRequest).userId; - const checks = await loadChecks(userId, host.id); - const results = checks.length ? await runChecks(client, checks) : []; - if (results.length) { - await recordHistory(userId, host.id, results); - for (const r of results) { - notifyAutomationHealthCheck( - host.id, - userId, - r.checkId, - r.ok, - r.detail ?? undefined, - ); - } - } - - const history = ( - await createCurrentHostHealthRepository().listHistory( - userId, - host.id, - 200, - ) - ).map((row) => ({ - checkId: row.checkId, - ts: row.ts, - ok: row.ok, - latencyMs: row.latencyMs, - detail: row.detail, - })); - return { checks, results, history }; - }, - ), + managerHandler(deps, "connect", "health_get", async (client, host) => { + const userId = host.actorId; + const checks = await loadChecks(repository, userId, host.id); + const results = checks.length ? await runChecks(client, checks) : []; + await record(userId, host.id, results); + const history = await repository.listHealth(userId, host.id, 200); + return { checks, results, history }; + }), ); app.post( "/host-metrics/managers/health/:id/config", validateHostId, managerHandler( - runOnHost, + deps, "connect", "health_config", async (_client, host, req) => { - const userId = (req as AuthenticatedRequest).userId; + const userId = host.actorId; const { checks, intervalSeconds } = req.body as { checks?: unknown; intervalSeconds?: number; @@ -206,13 +184,11 @@ export function registerHealthRoutes( intervalSeconds <= 86400 ? Math.round(intervalSeconds) : 300; - const now = new Date().toISOString(); - await createCurrentHostHealthRepository().upsertChecks( + await repository.saveChecks( userId, host.id, JSON.stringify(checks), interval, - now, ); return { success: true }; }, @@ -222,28 +198,11 @@ export function registerHealthRoutes( app.post( "/host-metrics/managers/health/:id/run", validateHostId, - managerHandler( - runOnHost, - "connect", - "health_run", - async (client, host, req) => { - const userId = (req as AuthenticatedRequest).userId; - const checks = await loadChecks(userId, host.id); - const results = await runChecks(client, checks); - if (results.length) { - await recordHistory(userId, host.id, results); - for (const r of results) { - notifyAutomationHealthCheck( - host.id, - userId, - r.checkId, - r.ok, - r.detail ?? undefined, - ); - } - } - return { results }; - }, - ), + managerHandler(deps, "connect", "health_run", async (client, host) => { + const checks = await loadChecks(repository, host.actorId, host.id); + const results = await runChecks(client, checks); + await record(host.actorId, host.id, results); + return { results }; + }), ); } diff --git a/plugins/host-metrics/src/backend/managers/index.ts b/plugins/host-metrics/src/backend/managers/index.ts index 6edf360ed..a0d287f1d 100644 --- a/plugins/host-metrics/src/backend/managers/index.ts +++ b/plugins/host-metrics/src/backend/managers/index.ts @@ -1,5 +1,5 @@ -import type { Express } from "express"; -import { detectPlatform } from "../../../../../src/backend/hosts/metrics-shared/platform.js"; +import { detectPlatform } from "@termix/plugin-sdk/host-commands"; +import type { Router } from "express"; import { managerHandler } from "./route-helpers.js"; import type { ManagerRoutesDeps } from "./types.js"; import { registerServiceRoutes } from "./services.js"; @@ -19,10 +19,10 @@ import { registerWireGuardRoutes } from "./wireguard.js"; * prefix on the stats app. All routes are on-demand (not polled). */ export function registerManagerRoutes( - app: Express, + app: Router, deps: ManagerRoutesDeps, ): void { - const { validateHostId, runOnHost } = deps; + const { validateHostId } = deps; /** * @openapi @@ -44,7 +44,7 @@ export function registerManagerRoutes( app.get( "/host-metrics/platform/:id", validateHostId, - managerHandler(runOnHost, "connect", "platform_detect", (client) => + managerHandler(deps, "connect", "platform_detect", (client) => detectPlatform(client), ), ); diff --git a/plugins/host-metrics/src/backend/managers/logs.ts b/plugins/host-metrics/src/backend/managers/logs.ts index 9e5f2a022..6f71192db 100644 --- a/plugins/host-metrics/src/backend/managers/logs.ts +++ b/plugins/host-metrics/src/backend/managers/logs.ts @@ -1,14 +1,11 @@ -import type { Express } from "express"; -import { execCommand } from "../../../../../src/backend/hosts/metrics-shared/common-utils.js"; import { + execCommand, execElevated, shellSingleQuote, -} from "../../../../../src/backend/hosts/metrics-shared/exec-elevated.js"; +} from "@termix/plugin-sdk/host-commands"; +import { isAllowedPath, isValidSystemdUnit } from "./validation.js"; +import type { Router } from "express"; import { managerHandler, ManagerInputError } from "./route-helpers.js"; -import { - isAllowedPath, - isValidSystemdUnit, -} from "../../../../../src/backend/hosts/metrics-shared/validation.js"; import type { ManagerRoutesDeps } from "./types.js"; /** Directories from which arbitrary log files may be tailed. */ @@ -44,14 +41,12 @@ export function buildJournalCommand(unit: string, lines: number): string { return `journalctl -u ${shellSingleQuote(unit)} -n ${lines} --no-pager`; } -export function registerLogRoutes( - app: Express, - { validateHostId, runOnHost }: ManagerRoutesDeps, -): void { +export function registerLogRoutes(app: Router, deps: ManagerRoutesDeps): void { + const { validateHostId } = deps; app.get( "/host-metrics/managers/logs/:id/files", validateHostId, - managerHandler(runOnHost, "connect", "logs_list", async (client) => { + managerHandler(deps, "connect", "logs_list", async (client) => { const { stdout } = await execCommand(client, LIST_LOGS_CMD, 10000); const found = stdout .split("\n") @@ -65,33 +60,28 @@ export function registerLogRoutes( app.get( "/host-metrics/managers/logs/:id", validateHostId, - managerHandler( - runOnHost, - "connect", - "logs_tail", - async (client, host, req) => { - const path = req.query.path as string | undefined; - const unit = req.query.unit as string | undefined; - const lines = clampLines(req.query.lines); + managerHandler(deps, "connect", "logs_tail", async (client, host, req) => { + const path = req.query.path as string | undefined; + const unit = req.query.unit as string | undefined; + const lines = clampLines(req.query.lines); - let cmd: string; - if (unit) { - if (!isValidSystemdUnit(unit)) - throw new ManagerInputError("Invalid unit"); - cmd = buildJournalCommand(unit, lines); - } else if (path) { - if (!isAllowedPath(path, LOG_PATH_ALLOWLIST)) { - throw new ManagerInputError("Path not allowed"); - } - cmd = buildTailCommand(path, lines); - } else { - throw new ManagerInputError("Provide a path or unit"); + let cmd: string; + if (unit) { + if (!isValidSystemdUnit(unit)) + throw new ManagerInputError("Invalid unit"); + cmd = buildJournalCommand(unit, lines); + } else if (path) { + if (!isAllowedPath(path, LOG_PATH_ALLOWLIST)) { + throw new ManagerInputError("Path not allowed"); } + cmd = buildTailCommand(path, lines); + } else { + throw new ManagerInputError("Provide a path or unit"); + } - // Try unprivileged; many logs need root (auth.log, etc.). - const result = await execElevated(client, cmd, host.sudoPassword); - return { content: result.stdout, lines }; - }, - ), + // Try unprivileged; many logs need root (auth.log, etc.). + const result = await execElevated(client, cmd, host.sudoPassword); + return { content: result.stdout, lines }; + }), ); } diff --git a/plugins/host-metrics/src/backend/managers/packages.ts b/plugins/host-metrics/src/backend/managers/packages.ts index 0ddf54af4..b2cb7e648 100644 --- a/plugins/host-metrics/src/backend/managers/packages.ts +++ b/plugins/host-metrics/src/backend/managers/packages.ts @@ -1,25 +1,26 @@ -import type { Express } from "express"; -import { execCommand } from "../../../../../src/backend/hosts/metrics-shared/common-utils.js"; -import { execElevated } from "../../../../../src/backend/hosts/metrics-shared/exec-elevated.js"; -import { managerHandler, ManagerInputError } from "./route-helpers.js"; -import { isValidPackageName } from "../../../../../src/backend/hosts/metrics-shared/validation.js"; -import { detectPlatform } from "../../../../../src/backend/hosts/metrics-shared/platform.js"; import { + execCommand, + execElevated, + detectPlatform, buildListUpgradableCommand, buildPackageActionCommand, parseUpgradable, type PackageAction, -} from "../../../../../src/backend/hosts/metrics-shared/package-commands.js"; +} from "@termix/plugin-sdk/host-commands"; +import { isValidPackageName } from "./validation.js"; +import type { Router } from "express"; +import { managerHandler, ManagerInputError } from "./route-helpers.js"; import type { ManagerRoutesDeps } from "./types.js"; export function registerPackageRoutes( - app: Express, - { validateHostId, runOnHost }: ManagerRoutesDeps, + app: Router, + deps: ManagerRoutesDeps, ): void { + const { validateHostId } = deps; app.get( "/host-metrics/managers/packages/:id", validateHostId, - managerHandler(runOnHost, "connect", "packages_list", async (client) => { + managerHandler(deps, "connect", "packages_list", async (client) => { const platform = await detectPlatform(client); const cmd = buildListUpgradableCommand(platform.pkg); if (!cmd) return { pkg: platform.pkg, upgradable: [] }; @@ -35,7 +36,7 @@ export function registerPackageRoutes( "/host-metrics/managers/packages/:id/action", validateHostId, managerHandler( - runOnHost, + deps, "connect", "packages_action", async (client, host, req) => { diff --git a/plugins/host-metrics/src/backend/managers/processes.ts b/plugins/host-metrics/src/backend/managers/processes.ts index 918c0afe7..f3eb67611 100644 --- a/plugins/host-metrics/src/backend/managers/processes.ts +++ b/plugins/host-metrics/src/backend/managers/processes.ts @@ -1,12 +1,7 @@ -import type { Express } from "express"; -import { execCommand } from "../../../../../src/backend/hosts/metrics-shared/common-utils.js"; -import { execElevated } from "../../../../../src/backend/hosts/metrics-shared/exec-elevated.js"; +import { execCommand, execElevated } from "@termix/plugin-sdk/host-commands"; +import { isValidPid, isValidSignal, type Signal } from "./validation.js"; +import type { Router } from "express"; import { managerHandler, ManagerInputError } from "./route-helpers.js"; -import { - isValidPid, - isValidSignal, - type Signal, -} from "../../../../../src/backend/hosts/metrics-shared/validation.js"; import type { ManagerRoutesDeps } from "./types.js"; export interface ProcessRow { @@ -54,9 +49,10 @@ export function buildKillCommand(pid: number, signal: Signal): string { } export function registerProcessRoutes( - app: Express, - { validateHostId, runOnHost }: ManagerRoutesDeps, + app: Router, + deps: ManagerRoutesDeps, ): void { + const { validateHostId } = deps; /** * @openapi * /host-metrics/managers/processes/{id}: @@ -74,7 +70,7 @@ export function registerProcessRoutes( app.get( "/host-metrics/managers/processes/:id", validateHostId, - managerHandler(runOnHost, "connect", "processes_list", async (client) => { + managerHandler(deps, "connect", "processes_list", async (client) => { const { stdout } = await execCommand(client, LIST_PROCESSES_CMD, 20000); return { processes: parseProcessList(stdout) }; }), @@ -109,7 +105,7 @@ export function registerProcessRoutes( "/host-metrics/managers/processes/:id/signal", validateHostId, managerHandler( - runOnHost, + deps, "connect", "processes_signal", async (client, host, req) => { diff --git a/plugins/host-metrics/src/backend/managers/route-helpers.ts b/plugins/host-metrics/src/backend/managers/route-helpers.ts index a6aad43b9..29fad7bb7 100644 --- a/plugins/host-metrics/src/backend/managers/route-helpers.ts +++ b/plugins/host-metrics/src/backend/managers/route-helpers.ts @@ -1,11 +1,10 @@ -import { getErrorMessage } from "../../../../../src/backend/utils/error-message.js"; +import { ElevationError } from "@termix/plugin-sdk/host-commands"; import type { Request, Response } from "express"; import type { Client } from "ssh2"; -import type { AuthenticatedRequest } from "../../../../../src/types/index.js"; -import { statsLogger } from "../../../../../src/backend/utils/logger.js"; -import { ElevationError } from "../../../../../src/backend/hosts/metrics-shared/exec-elevated.js"; +import type { PluginHostShareLevel } from "@termix/plugin-sdk/backend"; +import { errorMessage } from "../util.js"; +import type { MetricsLogger } from "../log.js"; import type { ManagerHost, RunOnHost } from "./types.js"; -import type { HostAction } from "../../../../../src/backend/utils/permission-manager.js"; export class AccessDeniedError extends Error { constructor(message = "No access to this host") { @@ -26,16 +25,15 @@ export class ManagerInputError extends Error { * given access level, and maps known errors to clean HTTP responses. */ export function managerHandler( - runOnHost: RunOnHost, - level: HostAction, + { runOnHost, log }: { runOnHost: RunOnHost; log: MetricsLogger }, + level: PluginHostShareLevel, operation: string, fn: (client: Client, host: ManagerHost, req: Request) => Promise, ) { return async (req: Request, res: Response) => { - const userId = (req as AuthenticatedRequest).userId; const hostId = parseInt(String(req.params.id), 10); try { - const result = await runOnHost(hostId, userId, level, (client, host) => + const result = await runOnHost(hostId, level, (client, host) => fn(client, host, req), ); return res.json(result); @@ -49,13 +47,13 @@ export function managerHandler( if (error instanceof ElevationError) { return res.status(403).json({ error: error.message, code: error.code }); } - statsLogger.error(`Manager operation failed: ${operation}`, { + log.error(`Manager operation failed: ${operation}`, { operation, hostId, error: error instanceof Error ? error.message : String(error), }); return res.status(500).json({ - error: getErrorMessage(error, "Operation failed"), + error: errorMessage(error, "Operation failed"), }); } }; diff --git a/plugins/host-metrics/src/backend/managers/services.ts b/plugins/host-metrics/src/backend/managers/services.ts index 367dfe5cc..dc1ec27b7 100644 --- a/plugins/host-metrics/src/backend/managers/services.ts +++ b/plugins/host-metrics/src/backend/managers/services.ts @@ -1,12 +1,11 @@ -import type { Express } from "express"; -import { execCommand } from "../../../../../src/backend/hosts/metrics-shared/common-utils.js"; -import { execElevated } from "../../../../../src/backend/hosts/metrics-shared/exec-elevated.js"; -import { managerHandler, ManagerInputError } from "./route-helpers.js"; +import { execCommand, execElevated } from "@termix/plugin-sdk/host-commands"; import { isValidSystemdUnit, isValidServiceAction, type ServiceAction, -} from "../../../../../src/backend/hosts/metrics-shared/validation.js"; +} from "./validation.js"; +import type { Router } from "express"; +import { managerHandler, ManagerInputError } from "./route-helpers.js"; import type { ManagerRoutesDeps } from "./types.js"; export interface SystemdService { @@ -50,9 +49,10 @@ export function buildServiceActionCommand( } export function registerServiceRoutes( - app: Express, - { validateHostId, runOnHost }: ManagerRoutesDeps, + app: Router, + deps: ManagerRoutesDeps, ): void { + const { validateHostId } = deps; /** * @openapi * /host-metrics/managers/services/{id}: @@ -70,7 +70,7 @@ export function registerServiceRoutes( app.get( "/host-metrics/managers/services/:id", validateHostId, - managerHandler(runOnHost, "connect", "services_list", async (client) => { + managerHandler(deps, "connect", "services_list", async (client) => { const { stdout } = await execCommand(client, LIST_SERVICES_CMD, 20000); return { services: parseServiceList(stdout) }; }), @@ -105,7 +105,7 @@ export function registerServiceRoutes( "/host-metrics/managers/services/:id/action", validateHostId, managerHandler( - runOnHost, + deps, "connect", "services_action", async (client, host, req) => { diff --git a/plugins/host-metrics/src/backend/managers/simple-reads.ts b/plugins/host-metrics/src/backend/managers/simple-reads.ts index dcf9db75c..6c22787a1 100644 --- a/plugins/host-metrics/src/backend/managers/simple-reads.ts +++ b/plugins/host-metrics/src/backend/managers/simple-reads.ts @@ -1,5 +1,5 @@ -import type { Express } from "express"; -import { execCommand } from "../../../../../src/backend/hosts/metrics-shared/common-utils.js"; +import { execCommand } from "@termix/plugin-sdk/host-commands"; +import type { Router } from "express"; import { managerHandler } from "./route-helpers.js"; import type { ManagerRoutesDeps } from "./types.js"; @@ -106,13 +106,14 @@ export function parseDfMounts(output: string): MountUsage[] { } export function registerSimpleReadRoutes( - app: Express, - { validateHostId, runOnHost }: ManagerRoutesDeps, + app: Router, + deps: ManagerRoutesDeps, ): void { + const { validateHostId } = deps; app.get( "/host-metrics/managers/top-memory/:id", validateHostId, - managerHandler(runOnHost, "connect", "top_memory", async (client) => { + managerHandler(deps, "connect", "top_memory", async (client) => { const { stdout } = await execCommand(client, TOP_MEM_CMD, 15000); return { processes: parseTopMemory(stdout) }; }), @@ -121,7 +122,7 @@ export function registerSimpleReadRoutes( app.get( "/host-metrics/managers/timers/:id", validateHostId, - managerHandler(runOnHost, "connect", "systemd_timers", async (client) => { + managerHandler(deps, "connect", "systemd_timers", async (client) => { const { stdout } = await execCommand(client, TIMERS_CMD, 15000); return { timers: parseTimers(stdout) }; }), @@ -130,7 +131,7 @@ export function registerSimpleReadRoutes( app.get( "/host-metrics/managers/disk-breakdown/:id", validateHostId, - managerHandler(runOnHost, "connect", "disk_breakdown", async (client) => { + managerHandler(deps, "connect", "disk_breakdown", async (client) => { const { stdout } = await execCommand(client, DF_CMD, 15000); return { mounts: parseDfMounts(stdout) }; }), diff --git a/plugins/host-metrics/src/backend/managers/ssl.ts b/plugins/host-metrics/src/backend/managers/ssl.ts index c251b884d..b683aeab8 100644 --- a/plugins/host-metrics/src/backend/managers/ssl.ts +++ b/plugins/host-metrics/src/backend/managers/ssl.ts @@ -1,16 +1,16 @@ -import type { Express } from "express"; -import { execCommand } from "../../../../../src/backend/hosts/metrics-shared/common-utils.js"; import { + execCommand, execElevated, shellSingleQuote, -} from "../../../../../src/backend/hosts/metrics-shared/exec-elevated.js"; -import { managerHandler, ManagerInputError } from "./route-helpers.js"; + detectPlatform, +} from "@termix/plugin-sdk/host-commands"; import { isValidDomain, isValidDnsProvider, isAllowedPath, -} from "../../../../../src/backend/hosts/metrics-shared/validation.js"; -import { detectPlatform } from "../../../../../src/backend/hosts/metrics-shared/platform.js"; +} from "./validation.js"; +import type { Router } from "express"; +import { managerHandler, ManagerInputError } from "./route-helpers.js"; import type { ManagerRoutesDeps } from "./types.js"; export type AcmeClient = "certbot" | "acme.sh"; @@ -149,14 +149,12 @@ export function buildRevokeCommand(client: AcmeClient, name: string): string { return `${ACMESH_BIN} --revoke -d ${d} && ${ACMESH_BIN} --remove -d ${d}`; } -export function registerSslRoutes( - app: Express, - { validateHostId, runOnHost }: ManagerRoutesDeps, -): void { +export function registerSslRoutes(app: Router, deps: ManagerRoutesDeps): void { + const { validateHostId } = deps; app.get( "/host-metrics/managers/ssl/:id", validateHostId, - managerHandler(runOnHost, "connect", "ssl_list", async (client, host) => { + managerHandler(deps, "connect", "ssl_list", async (client, host) => { const platform = await detectPlatform(client); const certs: CertInfo[] = []; if (platform.hasCertbot) { @@ -188,78 +186,68 @@ export function registerSslRoutes( app.post( "/host-metrics/managers/ssl/:id/issue", validateHostId, - managerHandler( - runOnHost, - "connect", - "ssl_issue", - async (client, host, req) => { - const body = req.body as Partial; - if (body.client !== "certbot" && body.client !== "acme.sh") { - throw new ManagerInputError("Invalid ACME client"); - } - if (!Array.isArray(body.domains) || body.domains.length === 0) { - throw new ManagerInputError("At least one domain is required"); - } - for (const d of body.domains) { - if (!isValidDomain(d)) - throw new ManagerInputError(`Invalid domain: ${d}`); - } - const challenge = body.challenge; - if ( - challenge !== "http-standalone" && - challenge !== "http-webroot" && - challenge !== "dns" - ) { - throw new ManagerInputError("Invalid challenge type"); - } - if (challenge === "dns" && !isValidDnsProvider(body.dnsProvider)) { - throw new ManagerInputError("Invalid DNS provider"); - } - if ( - challenge === "http-webroot" && - !isAllowedPath(body.webroot, ["/var/www", "/srv", "/usr/share/nginx"]) - ) { - throw new ManagerInputError("Invalid or disallowed webroot path"); - } - const cmd = buildIssueCommand(body as IssueRequest); - const result = await execElevated(client, cmd, host.sudoPassword, { - forceSudo: true, - timeoutMs: 300000, - }); - return { - success: result.code === 0, - output: (result.stdout || result.stderr).slice(-8000), - }; - }, - ), + managerHandler(deps, "connect", "ssl_issue", async (client, host, req) => { + const body = req.body as Partial; + if (body.client !== "certbot" && body.client !== "acme.sh") { + throw new ManagerInputError("Invalid ACME client"); + } + if (!Array.isArray(body.domains) || body.domains.length === 0) { + throw new ManagerInputError("At least one domain is required"); + } + for (const d of body.domains) { + if (!isValidDomain(d)) + throw new ManagerInputError(`Invalid domain: ${d}`); + } + const challenge = body.challenge; + if ( + challenge !== "http-standalone" && + challenge !== "http-webroot" && + challenge !== "dns" + ) { + throw new ManagerInputError("Invalid challenge type"); + } + if (challenge === "dns" && !isValidDnsProvider(body.dnsProvider)) { + throw new ManagerInputError("Invalid DNS provider"); + } + if ( + challenge === "http-webroot" && + !isAllowedPath(body.webroot, ["/var/www", "/srv", "/usr/share/nginx"]) + ) { + throw new ManagerInputError("Invalid or disallowed webroot path"); + } + const cmd = buildIssueCommand(body as IssueRequest); + const result = await execElevated(client, cmd, host.sudoPassword, { + forceSudo: true, + timeoutMs: 300000, + }); + return { + success: result.code === 0, + output: (result.stdout || result.stderr).slice(-8000), + }; + }), ); app.post( "/host-metrics/managers/ssl/:id/renew", validateHostId, - managerHandler( - runOnHost, - "connect", - "ssl_renew", - async (client, host, req) => { - const { client: acmeClient, dryRun } = req.body as { - client?: AcmeClient; - dryRun?: boolean; - }; - if (acmeClient !== "certbot" && acmeClient !== "acme.sh") { - throw new ManagerInputError("Invalid ACME client"); - } - const cmd = buildRenewCommand(acmeClient, !!dryRun); - const result = await execElevated(client, cmd, host.sudoPassword, { - forceSudo: true, - timeoutMs: 300000, - }); - return { - success: result.code === 0, - output: (result.stdout || result.stderr).slice(-8000), - }; - }, - ), + managerHandler(deps, "connect", "ssl_renew", async (client, host, req) => { + const { client: acmeClient, dryRun } = req.body as { + client?: AcmeClient; + dryRun?: boolean; + }; + if (acmeClient !== "certbot" && acmeClient !== "acme.sh") { + throw new ManagerInputError("Invalid ACME client"); + } + const cmd = buildRenewCommand(acmeClient, !!dryRun); + const result = await execElevated(client, cmd, host.sudoPassword, { + forceSudo: true, + timeoutMs: 300000, + }); + return { + success: result.code === 0, + output: (result.stdout || result.stderr).slice(-8000), + }; + }), ); /** @@ -290,31 +278,26 @@ export function registerSslRoutes( app.post( "/host-metrics/managers/ssl/:id/revoke", validateHostId, - managerHandler( - runOnHost, - "connect", - "ssl_revoke", - async (client, host, req) => { - const { client: acmeClient, name } = req.body as { - client?: AcmeClient; - name?: string; - }; - if (acmeClient !== "certbot" && acmeClient !== "acme.sh") { - throw new ManagerInputError("Invalid ACME client"); - } - if (!isValidCertName(name)) { - throw new ManagerInputError("Invalid certificate name"); - } - const cmd = buildRevokeCommand(acmeClient, name); - const result = await execElevated(client, cmd, host.sudoPassword, { - forceSudo: true, - timeoutMs: 120000, - }); - return { - success: result.code === 0, - output: (result.stdout || result.stderr).slice(-8000), - }; - }, - ), + managerHandler(deps, "connect", "ssl_revoke", async (client, host, req) => { + const { client: acmeClient, name } = req.body as { + client?: AcmeClient; + name?: string; + }; + if (acmeClient !== "certbot" && acmeClient !== "acme.sh") { + throw new ManagerInputError("Invalid ACME client"); + } + if (!isValidCertName(name)) { + throw new ManagerInputError("Invalid certificate name"); + } + const cmd = buildRevokeCommand(acmeClient, name); + const result = await execElevated(client, cmd, host.sudoPassword, { + forceSudo: true, + timeoutMs: 120000, + }); + return { + success: result.code === 0, + output: (result.stdout || result.stderr).slice(-8000), + }; + }), ); } diff --git a/plugins/host-metrics/src/backend/managers/types.ts b/plugins/host-metrics/src/backend/managers/types.ts index 1169d06d8..ede71aa9a 100644 --- a/plugins/host-metrics/src/backend/managers/types.ts +++ b/plugins/host-metrics/src/backend/managers/types.ts @@ -1,28 +1,43 @@ import type { Client } from "ssh2"; import type { RequestHandler } from "express"; -import type { HostAction } from "../../../../../src/backend/utils/permission-manager.js"; +import type { PluginHostShareLevel } from "@termix/plugin-sdk/backend"; +import type { HostMetricsRepository } from "../repository.js"; +import type { MetricsLogger } from "../log.js"; /** Minimal host shape managers need (includes the decrypted sudo password). */ export interface ManagerHost { id: number; userId: string; + /** The user making the request, who may not own the host. */ + actorId: string; sudoPassword?: string; enableDocker?: boolean; } /** * Runs `fn` against a pooled SSH connection for the host, after verifying the - * user has at least `level` access. Resolves the host (with sudoPassword) so - * managers can elevate. Rejects with an access error if not permitted. + * acting user has at least `level` access. Resolves the host (with + * sudoPassword) so managers can elevate. Rejects with an access error if not + * permitted. */ export type RunOnHost = ( hostId: number, - userId: string, - level: HostAction, + level: PluginHostShareLevel, fn: (client: Client, host: ManagerHost) => Promise, ) => Promise; +export interface HealthCheckEvent { + hostId: number; + userId: string; + checkId: string; + ok: boolean; + detail?: string; +} + export interface ManagerRoutesDeps { validateHostId: RequestHandler; runOnHost: RunOnHost; + log: MetricsLogger; + repository: HostMetricsRepository; + onHealthCheck: (event: HealthCheckEvent) => void; } diff --git a/plugins/host-metrics/src/backend/managers/users.ts b/plugins/host-metrics/src/backend/managers/users.ts index 34ae6ef06..7b599e6b3 100644 --- a/plugins/host-metrics/src/backend/managers/users.ts +++ b/plugins/host-metrics/src/backend/managers/users.ts @@ -1,11 +1,7 @@ -import type { Express } from "express"; -import { execCommand } from "../../../../../src/backend/hosts/metrics-shared/common-utils.js"; -import { execElevated } from "../../../../../src/backend/hosts/metrics-shared/exec-elevated.js"; +import { execCommand, execElevated } from "@termix/plugin-sdk/host-commands"; +import { isValidUsername, isValidGroupName } from "./validation.js"; +import type { Router } from "express"; import { managerHandler, ManagerInputError } from "./route-helpers.js"; -import { - isValidUsername, - isValidGroupName, -} from "../../../../../src/backend/hosts/metrics-shared/validation.js"; import type { ManagerRoutesDeps } from "./types.js"; export interface SystemUser { @@ -75,14 +71,12 @@ export function parseSudoers(output: string): string[] { export type UserAction = "create" | "delete" | "addToGroup" | "removeFromGroup"; -export function registerUserRoutes( - app: Express, - { validateHostId, runOnHost }: ManagerRoutesDeps, -): void { +export function registerUserRoutes(app: Router, deps: ManagerRoutesDeps): void { + const { validateHostId } = deps; app.get( "/host-metrics/managers/users/:id", validateHostId, - managerHandler(runOnHost, "connect", "users_list", async (client) => { + managerHandler(deps, "connect", "users_list", async (client) => { const [passwd, groups, sudoers] = await Promise.all([ execCommand(client, READ_USERS_CMD, 15000), execCommand(client, READ_GROUPS_CMD, 15000), @@ -100,7 +94,7 @@ export function registerUserRoutes( "/host-metrics/managers/users/:id/action", validateHostId, managerHandler( - runOnHost, + deps, "connect", "users_action", async (client, host, req) => { diff --git a/src/backend/hosts/metrics-shared/validation.ts b/plugins/host-metrics/src/backend/managers/validation.ts similarity index 100% rename from src/backend/hosts/metrics-shared/validation.ts rename to plugins/host-metrics/src/backend/managers/validation.ts diff --git a/plugins/host-metrics/src/backend/managers/wireguard.ts b/plugins/host-metrics/src/backend/managers/wireguard.ts index 15b9d754b..649f48589 100644 --- a/plugins/host-metrics/src/backend/managers/wireguard.ts +++ b/plugins/host-metrics/src/backend/managers/wireguard.ts @@ -1,11 +1,11 @@ -import type { Express } from "express"; -import { execElevated } from "../../../../../src/backend/hosts/metrics-shared/exec-elevated.js"; -import { managerHandler, ManagerInputError } from "./route-helpers.js"; -import type { ManagerRoutesDeps } from "./types.js"; +import { execElevated } from "@termix/plugin-sdk/host-commands"; import { isValidWireGuardInterface, isValidWireGuardAction, -} from "../../../../../src/backend/hosts/metrics-shared/validation.js"; +} from "./validation.js"; +import type { Router } from "express"; +import { managerHandler, ManagerInputError } from "./route-helpers.js"; +import type { ManagerRoutesDeps } from "./types.js"; export interface WireGuardPeer { publicKey: string; @@ -117,9 +117,10 @@ export function parseWireGuardData(output: string): WireGuardData { } export function registerWireGuardRoutes( - app: Express, - { validateHostId, runOnHost }: ManagerRoutesDeps, + app: Router, + deps: ManagerRoutesDeps, ): void { + const { validateHostId } = deps; /** * @openapi * /host-metrics/managers/wireguard/{id}: @@ -140,23 +141,13 @@ export function registerWireGuardRoutes( app.get( "/host-metrics/managers/wireguard/:id", validateHostId, - managerHandler( - runOnHost, - "connect", - "wireguard_read", - async (client, host) => { - const result = await execElevated( - client, - PROBE_CMD, - host.sudoPassword, - { - forceSudo: false, - timeoutMs: 15000, - }, - ); - return parseWireGuardData(result.stdout); - }, - ), + managerHandler(deps, "connect", "wireguard_read", async (client, host) => { + const result = await execElevated(client, PROBE_CMD, host.sudoPassword, { + forceSudo: false, + timeoutMs: 15000, + }); + return parseWireGuardData(result.stdout); + }), ); /** @@ -192,7 +183,7 @@ export function registerWireGuardRoutes( "/host-metrics/managers/wireguard/:id/action", validateHostId, managerHandler( - runOnHost, + deps, "connect", "wireguard_action", async (client, host, req) => { diff --git a/plugins/host-metrics/src/backend/poller.ts b/plugins/host-metrics/src/backend/poller.ts new file mode 100644 index 000000000..31a4538c6 --- /dev/null +++ b/plugins/host-metrics/src/backend/poller.ts @@ -0,0 +1,469 @@ +import type { + PluginContext, + PluginHostStatusEntry, +} from "@termix/plugin-sdk/backend"; +import { readHostMetricsSettings } from "../shared/stats-widgets.js"; +import { + collectMetrics, + isHostKeyVerificationError, + type CollectDeps, + type CollectedMetrics, +} from "./collect.js"; +import { supportsMetrics, type MetricsHost } from "./helpers.js"; +import type { MetricsLogger } from "./log.js"; +import type { HostMetricsRepository } from "./repository.js"; +import type { MetricsViewer } from "./sessions.js"; +import { + canStartInitialMetrics, + metricsConcurrencyFor, + type MetricsState, +} from "./state.js"; +import { clearCpuSampleCache } from "./widgets/cpu-collector.js"; +import { clearNetworkSampleCache } from "./widgets/network-collector.js"; + +export const DEFAULT_METRICS_INTERVAL = 30; +export const DEFAULT_RETENTION_DAYS = 7; +/** Viewers that stop sending heartbeats are dropped after this long. */ +const VIEWER_TIMEOUT_MS = 120_000; + +export const TOPIC_SNAPSHOT = "plugin.host-metrics.snapshot"; +export const TOPIC_STATUS = "plugin.host-metrics.status"; +export const TOPIC_HEALTH_CHECK = "plugin.host-metrics.health-check"; + +/** Where collection stands for a host, sent when it changes. */ +export type CollectionState = + "collecting" | "auth_failed" | "host_key_changed" | "unreachable"; + +export interface SnapshotPayload { + hostId: number; + ownerUserId: string; + metrics: CollectedMetrics; +} + +export interface CollectionStatusPayload { + hostId: number; + ownerUserId: string; + state: CollectionState; + previous: CollectionState | null; +} + +interface Polled { + host: MetricsHost; + viewerUserId: string; + stop: () => void; +} + +export interface PollerDeps extends CollectDeps { + ctx: PluginContext; + log: MetricsLogger; + repository: HostMetricsRepository; +} + +/** + * Heavy SSH metrics for hosts somebody is looking at. Polling starts with the + * first viewer and stops with the last one; a viewer is a tab, a dashboard + * card, or another plugin through the "host-metrics.viewers" service. + */ +export class MetricsPoller { + private readonly polled = new Map(); + private readonly metrics = new Map< + number, + { data: CollectedMetrics; timestamp: number } + >(); + private readonly viewers = new Map>(); + private readonly viewerDetails = new Map(); + private readonly inFlight = new Set(); + private readonly initialRequested = new Set(); + private readonly collectionState = new Map(); + + constructor(private readonly deps: PollerDeps) {} + + private get ctx() { + return this.deps.ctx; + } + + private get state(): MetricsState { + return this.deps.state; + } + + getMetrics(hostId: number) { + return this.metrics.get(hostId); + } + + isPolling(hostId: number): boolean { + return this.polled.has(hostId); + } + + hasViewers(hostId: number): boolean { + return (this.viewers.get(hostId)?.size ?? 0) > 0; + } + + registerViewer(hostId: number, sessionId: string, userId: string): void { + let set = this.viewers.get(hostId); + if (!set) { + set = new Set(); + this.viewers.set(hostId, set); + } + set.add(sessionId); + this.viewerDetails.set(sessionId, { + sessionId, + userId, + hostId, + lastHeartbeat: Date.now(), + }); + + if (set.size === 1) { + // Never let a failed start reach the request that registered it. + void this.start(hostId, userId).catch((error) => { + this.deps.log.warn("Could not start metrics polling", { + operation: "start_metrics_unhandled", + hostId, + error: error instanceof Error ? error.message : String(error), + }); + }); + } + } + + /** False when the viewer is gone, so the caller registers again. */ + updateHeartbeat(sessionId: string, userId?: string): boolean { + const viewer = this.viewerDetails.get(sessionId); + if (!viewer || (userId && viewer.userId !== userId)) return false; + viewer.lastHeartbeat = Date.now(); + return true; + } + + unregisterViewer(hostId: number, sessionId: string, userId?: string): void { + const viewer = this.viewerDetails.get(sessionId); + if (viewer && userId && viewer.userId !== userId) return; + const set = this.viewers.get(hostId); + if (set) { + set.delete(sessionId); + if (set.size === 0) { + this.viewers.delete(hostId); + this.stop(hostId, false); + } + } + this.viewerDetails.delete(sessionId); + } + + /** Stops a host's polling without dropping its viewers. */ + pause(hostId: number): void { + this.stop(hostId, false); + } + + cleanupInactiveViewers(now = Date.now()): void { + for (const [sessionId, viewer] of [...this.viewerDetails.entries()]) { + if (now - viewer.lastHeartbeat > VIEWER_TIMEOUT_MS) { + this.unregisterViewer(viewer.hostId, sessionId); + } + } + } + + /** A host's details changed: drop what was cached and start over. */ + async hostUpdated(hostId: number): Promise { + this.state.hostCache.invalidate(hostId); + this.state.metricsCache.clear(hostId); + this.ctx.ssh.dropPooled("stats", hostId); + const polled = this.polled.get(hostId); + if (polled) await this.start(hostId, polled.viewerUserId); + } + + hostDeleted(hostId: number): void { + this.stop(hostId, true); + for (const sessionId of this.viewers.get(hostId) ?? []) { + this.viewerDetails.delete(sessionId); + } + this.viewers.delete(hostId); + this.collectionState.delete(hostId); + this.state.authFailures.reset(hostId); + this.state.backoff.reset(hostId); + } + + /** Core's status check says the host answers again. */ + hostStatusChanged(hostId: number, status: string): void { + if (status === "offline") return; + const polled = this.polled.get(hostId); + if (polled) this.scheduleInitialPoll(polled.host); + } + + hostKeyAccepted(hostId: number): void { + this.state.authFailures.resetHostKeyFailure(hostId); + } + + /** Re-times every running host, after the admin interval changed. */ + async retimeAll(): Promise { + for (const [hostId, polled] of [...this.polled.entries()]) { + await this.start(hostId, polled.viewerUserId); + } + } + + dispose(): void { + for (const hostId of [...this.polled.keys()]) this.stop(hostId, true); + this.viewers.clear(); + this.viewerDetails.clear(); + clearCpuSampleCache(); + clearNetworkSampleCache(); + } + + /** The host as `userId` may reach it, cached for a few minutes. */ + async resolve(hostId: number, userId: string): Promise { + const cached = this.state.hostCache.get( + hostId, + userId, + ) as MetricsHost | null; + if (cached) return cached; + const host = (await this.ctx.asUser(userId, () => + this.ctx.ssh.resolveHost(hostId), + )) as MetricsHost | null; + if (!host) return null; + this.state.hostCache.set(hostId, userId, host); + return host; + } + + async settingsFor(hostId: number) { + const settings = readHostMetricsSettings( + await this.ctx.settings.getAll("host", hostId), + ); + const adminInterval = Number( + await this.ctx.settings.get("metricsInterval"), + ); + return { + ...settings, + intervalSeconds: + settings.metricsInterval ?? + (Number.isInteger(adminInterval) && adminInterval >= 5 + ? adminInterval + : DEFAULT_METRICS_INTERVAL), + }; + } + + private async start(hostId: number, userId: string): Promise { + const host = await this.resolve(hostId, userId); + this.stop(hostId, false); + if (!host || !supportsMetrics(host, this.ctx.ssh)) return; + + const settings = await this.settingsFor(hostId); + if (!settings.metricsEnabled) { + this.metrics.delete(hostId); + return; + } + + const intervalMs = settings.intervalSeconds * 1000; + const stop = this.ctx.schedule.every( + intervalMs, + () => { + const latest = this.polled.get(hostId); + if (latest) this.schedulePoll(latest.host); + }, + // Spread timers so a fleet does not fire on the same second. + { jitterMs: Math.min(intervalMs * 0.2, 15_000) }, + ); + this.polled.set(hostId, { host, viewerUserId: userId, stop }); + this.syncConcurrency(); + this.scheduleInitialPoll(host); + } + + private stop(hostId: number, clearData: boolean): void { + const polled = this.polled.get(hostId); + if (polled) { + polled.stop(); + this.polled.delete(hostId); + this.syncConcurrency(); + } + this.inFlight.delete(hostId); + if (clearData) { + this.metrics.delete(hostId); + this.state.hostCache.invalidate(hostId); + clearCpuSampleCache(hostId); + clearNetworkSampleCache(hostId); + } + } + + /** Keeps poll concurrency matched to how many hosts are being polled. */ + private syncConcurrency(): void { + const target = metricsConcurrencyFor(this.polled.size); + const limiter = this.state.metricsLimiter; + if (target === limiter.limit) return; + const previous = limiter.limit; + limiter.setLimit(target); + this.deps.log.info( + `Metrics poll concurrency ${previous} -> ${target} for ${this.polled.size} host(s)`, + { operation: "metrics_concurrency_resize" }, + ); + } + + /** + * The first heavy sample waits for core's cheap status check to say the + * host answers, so a whole fleet registering at once does not open a + * burst of SSH connections to hosts that are down. + */ + private scheduleInitialPoll(host: MetricsHost): void { + if (this.metrics.has(host.id) || this.initialRequested.has(host.id)) { + return; + } + this.initialRequested.add(host.id); + void this.state.initialLimiter + .run(async () => { + let status: PluginHostStatusEntry | null = this.ctx.hosts.status.get( + host.id, + ); + if (!status) status = await this.ctx.hosts.status.check(host.id); + if (!canStartInitialMetrics(status?.status, this.hasViewers(host.id))) { + return; + } + if (this.inFlight.has(host.id)) return; + this.inFlight.add(host.id); + try { + await this.state.metricsLimiter.run(() => this.poll(host)); + } finally { + this.inFlight.delete(host.id); + } + }) + .catch((error) => { + this.deps.log.error("Initial metrics polling failed", error, { + operation: "initial_metrics_poll_unhandled", + hostId: host.id, + }); + }) + .finally(() => this.initialRequested.delete(host.id)); + } + + private schedulePoll(host: MetricsHost): void { + if (this.inFlight.has(host.id)) return; + this.inFlight.add(host.id); + void this.state.metricsLimiter + .run(() => this.poll(host)) + .catch((error) => { + this.deps.log.error("Metrics polling failed", error, { + operation: "metrics_poll_unhandled", + hostId: host.id, + }); + }) + .finally(() => this.inFlight.delete(host.id)); + } + + /** One sample for one host. Public for tests. */ + async poll(host: MetricsHost): Promise { + const { state, log } = this.deps; + if (!this.polled.has(host.id)) return; + if (state.authFailures.shouldSkip(host.id)) return; + if (state.backoff.shouldSkip(host.id)) return; + + const settings = await this.settingsFor(host.id); + if (!settings.metricsEnabled) return; + + let authenticated = false; + try { + const metrics = await collectMetrics(this.deps, host, settings, () => { + authenticated = true; + }); + this.metrics.set(host.id, { data: metrics, timestamp: Date.now() }); + this.ctx.hosts.status.reportLogin(host.id, { ok: true }); + this.setCollectionState(host, "collecting"); + await this.recordSample(host.id, metrics); + this.ctx.events.emit(TOPIC_SNAPSHOT, { + hostId: host.id, + ownerUserId: host.userId, + metrics, + } satisfies SnapshotPayload); + state.backoff.reset(host.id); + state.authFailures.reset(host.id); + } catch (error) { + const hostKeyChanged = isHostKeyVerificationError(error); + const message = error instanceof Error ? error.message : ""; + const isAuthError = /authentication|permission denied/i.test(message); + + if (!authenticated) { + this.ctx.hosts.status.reportLogin(host.id, { + ok: false, + hostKeyChanged, + }); + } + this.setCollectionState( + host, + hostKeyChanged + ? "host_key_changed" + : isAuthError + ? "auth_failed" + : "unreachable", + ); + + if (isAuthError) { + // collectMetrics already recorded it; log the first occurrence only. + if (!state.authFailures.shouldSkip(host.id)) { + log.error("Stats collector connection failed", error, { + operation: "stats_connect_failed", + hostId: host.id, + }); + } + return; + } + + if (hostKeyChanged) { + state.authFailures.recordFailure(host.id, "HOST_KEY", true); + log.error("Stats collector host key verification failed", error, { + operation: "stats_host_key_verification_failed", + hostId: host.id, + }); + return; + } + + state.backoff.recordFailure(host.id); + // Only log when a new retry window opens, not on every skipped poll. + const backoff = state.backoff.getBackoffInfo(host.id); + if (backoff !== null && !backoff.includes("polling suspended")) { + log.error("Stats collector connection failed", error, { + operation: "stats_connect_failed", + hostId: host.id, + }); + } + } + } + + private setCollectionState(host: MetricsHost, next: CollectionState): void { + const previous = this.collectionState.get(host.id) ?? null; + if (previous === next) return; + this.collectionState.set(host.id, next); + this.ctx.events.emit(TOPIC_STATUS, { + hostId: host.id, + ownerUserId: host.userId, + state: next, + previous, + } satisfies CollectionStatusPayload); + } + + private async recordSample( + hostId: number, + metrics: CollectedMetrics, + ): Promise { + try { + const iface = metrics.network?.interfaces?.[0]; + const bytes = (value: string | null | undefined) => { + const parsed = value ? parseInt(value, 10) : NaN; + return Number.isNaN(parsed) ? null : parsed; + }; + const retention = Number( + await this.ctx.settings.get("historyRetentionDays"), + ); + await this.deps.repository.recordSample( + { + hostId, + cpuPercent: metrics.cpu?.percent ?? null, + memPercent: metrics.memory?.percent ?? null, + diskPercent: metrics.disk?.percent ?? null, + netRxBytes: bytes(iface?.rxBytes), + netTxBytes: bytes(iface?.txBytes), + }, + Number.isInteger(retention) && retention >= 1 + ? Math.min(retention, 90) + : DEFAULT_RETENTION_DAYS, + ); + } catch (error) { + this.deps.log.warn("Failed to write metrics history", { + operation: "insert_metrics_history", + hostId, + error: error instanceof Error ? error.message : String(error), + }); + } + } +} diff --git a/plugins/host-metrics/src/backend/preferences-routes.ts b/plugins/host-metrics/src/backend/preferences-routes.ts deleted file mode 100644 index b0fd17d47..000000000 --- a/plugins/host-metrics/src/backend/preferences-routes.ts +++ /dev/null @@ -1,230 +0,0 @@ -import type { Express, RequestHandler } from "express"; -import type { AuthenticatedRequest } from "../../../../src/types/index.js"; -import { createCurrentHostMetricsPreferenceRepository } from "../../../../src/backend/database/repositories/factory.js"; -import { statsLogger } from "../../../../src/backend/utils/logger.js"; -import { - deriveEnabledWidgets, - defaultLayoutFromWidgets, - type HostMetricsLayout, -} from "../../../../src/types/host-metrics.js"; -import type { HostAction } from "../../../../src/backend/utils/permission-manager.js"; - -interface PrefStatsConfig { - enabledWidgets?: string[]; -} - -interface PrefHost { - id: number; - userId: string; - statsConfig?: unknown; -} - -type HostMetricsPreferencesRoutesDeps = { - validateHostId: RequestHandler; - fetchHostById: (hostId: number, userId: string) => Promise; - parseStatsConfig: (statsConfig: unknown) => PrefStatsConfig; - canAccessHost: ( - userId: string, - hostId: number, - level: HostAction, - ) => Promise; -}; - -function sanitizeLayout(input: unknown): HostMetricsLayout | null { - if (!input || typeof input !== "object") return null; - const obj = input as Record; - if (!Array.isArray(obj.slots)) return null; - const columns = - typeof obj.columns === "number" && obj.columns >= 1 && obj.columns <= 4 - ? Math.round(obj.columns) - : 3; - const slots = obj.slots - .filter((s): s is Record => !!s && typeof s === "object") - .map((s, i) => ({ - id: String(s.id), - order: typeof s.order === "number" ? s.order : i, - colSpan: - s.colSpan === 1 || s.colSpan === 2 || s.colSpan === 3 ? s.colSpan : 1, - height: - typeof s.height === "number" && s.height > 0 - ? Math.round(s.height) - : null, - })) - .filter((s) => s.id && s.id !== "undefined"); - return { slots, columns } as HostMetricsLayout; -} - -export function registerHostMetricsPreferencesRoutes( - app: Express, - { - validateHostId, - fetchHostById, - parseStatsConfig, - canAccessHost, - }: HostMetricsPreferencesRoutesDeps, -): void { - /** - * @openapi - * /host-metrics/preferences/{id}: - * get: - * summary: Get the Host Metrics layout for a host - * description: Returns the current user's saved card layout for the host, or a default layout derived from the host's enabled widgets when none is saved. - * tags: - * - Host Metrics - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: integer - * responses: - * 200: - * description: The layout for this host. - * 403: - * description: No access to this host. - * 404: - * description: Host not found. - */ - app.get("/host-metrics/preferences/:id", validateHostId, async (req, res) => { - const userId = (req as AuthenticatedRequest).userId; - const hostId = parseInt(String(req.params.id), 10); - try { - if (!(await canAccessHost(userId, hostId, "connect"))) { - return res.status(403).json({ error: "No access to this host" }); - } - const host = await fetchHostById(hostId, userId); - if (!host) { - return res.status(404).json({ error: "Host not found" }); - } - - const row = - await createCurrentHostMetricsPreferenceRepository().findByUserAndHost( - userId, - hostId, - ); - - if (row?.layout) { - try { - const parsed = sanitizeLayout(JSON.parse(row.layout)); - if (parsed) return res.json({ layout: parsed }); - } catch { - // fall through to default - } - } - - const statsConfig = parseStatsConfig(host.statsConfig); - const layout = defaultLayoutFromWidgets(statsConfig.enabledWidgets ?? []); - return res.json({ layout }); - } catch (error) { - statsLogger.error("Failed to fetch host metrics preferences", { - operation: "host_metrics_prefs_fetch_error", - hostId, - error: error instanceof Error ? error.message : String(error), - }); - return res - .status(500) - .json({ error: "Failed to fetch host metrics preferences" }); - } - }); - - /** - * @openapi - * /host-metrics/preferences/{id}: - * post: - * summary: Save the Host Metrics layout for a host - * description: Persists the current user's card layout for the host and keeps statsConfig.enabledWidgets in sync (for hosts the user owns) so the mobile app keeps working. - * tags: - * - Host Metrics - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: integer - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * slots: - * type: array - * columns: - * type: integer - * responses: - * 200: - * description: Layout saved. - * 400: - * description: Invalid layout. - * 403: - * description: No access to this host. - */ - app.post( - "/host-metrics/preferences/:id", - validateHostId, - async (req, res) => { - const userId = (req as AuthenticatedRequest).userId; - const hostId = parseInt(String(req.params.id), 10); - try { - if (!(await canAccessHost(userId, hostId, "connect"))) { - return res.status(403).json({ error: "No access to this host" }); - } - const host = await fetchHostById(hostId, userId); - if (!host) { - return res.status(404).json({ error: "Host not found" }); - } - - const layout = sanitizeLayout(req.body); - if (!layout) { - return res.status(400).json({ error: "Invalid layout" }); - } - - const now = new Date().toISOString(); - const layoutJson = JSON.stringify(layout); - - await createCurrentHostMetricsPreferenceRepository().upsertLayout( - userId, - hostId, - layoutJson, - now, - ); - - // Keep statsConfig.enabledWidgets in sync (mobile contract) for hosts the - // user owns. stats_config is plain JSON text (not an encrypted field). - if (host.userId === userId) { - try { - const current = parseStatsConfig(host.statsConfig); - const merged = { - ...current, - enabledWidgets: deriveEnabledWidgets(layout.slots), - }; - await createCurrentHostMetricsPreferenceRepository().updateHostStatsConfig( - userId, - hostId, - JSON.stringify(merged), - ); - } catch (syncErr) { - statsLogger.warn("Failed to sync enabledWidgets from layout", { - operation: "host_metrics_prefs_sync_widgets", - hostId, - error: - syncErr instanceof Error ? syncErr.message : String(syncErr), - }); - } - } - - return res.json({ success: true }); - } catch (error) { - statsLogger.error("Failed to save host metrics preferences", { - operation: "host_metrics_prefs_save_error", - hostId, - error: error instanceof Error ? error.message : String(error), - }); - return res - .status(500) - .json({ error: "Failed to save host metrics preferences" }); - } - }, - ); -} diff --git a/plugins/host-metrics/src/backend/repository.ts b/plugins/host-metrics/src/backend/repository.ts new file mode 100644 index 000000000..abbbd975c --- /dev/null +++ b/plugins/host-metrics/src/backend/repository.ts @@ -0,0 +1,233 @@ +import { and, asc, desc, eq, gte, lt, lte, notInArray } from "drizzle-orm"; +import type { PluginDatabase } from "@termix/plugin-sdk/backend"; +import { + hostHealthChecks, + hostHealthHistory, + hostMetricsHistory, + hostMetricsPreferences, +} from "./tables.js"; + +/* eslint-disable @typescript-eslint/no-explicit-any */ +// ctx.db.define hands tables back untyped, and the drizzle handle is the +// server's own. Typed at this module's edge instead. +type Table = any; +type Drizzle = any; +/* eslint-enable @typescript-eslint/no-explicit-any */ + +export interface HealthResultInput { + checkId: string; + ok: boolean; + latencyMs: number | null; + detail: string; +} + +export interface HealthHistoryRow { + checkId: string; + ts: string; + ok: boolean; + latencyMs: number | null; + detail: string | null; +} + +export interface MetricsHistoryInput { + hostId: number; + cpuPercent: number | null; + memPercent: number | null; + diskPercent: number | null; + netRxBytes: number | null; + netTxBytes: number | null; +} + +export interface MetricsHistoryRow { + ts: string; + cpuPercent: number | null; + memPercent: number | null; + diskPercent: number | null; + netRxBytes: number | null; + netTxBytes: number | null; +} + +/** Timestamps are text in `YYYY-MM-DD HH:MM:SS` UTC, which sorts in time order. */ +export function sqlTimestamp(date: Date): string { + return date.toISOString().slice(0, 19).replace("T", " "); +} + +export type HostMetricsRepository = Awaited< + ReturnType +>; + +/** + * Written without RETURNING so it runs the same on all three engines: an + * upsert reads the row by its unique (user, host) pair first. + */ +export async function createHostMetricsRepository(db: PluginDatabase) { + const preferences: Table = await db.define(hostMetricsPreferences); + const checks: Table = await db.define(hostHealthChecks); + const health: Table = await db.define(hostHealthHistory); + const history: Table = await db.define(hostMetricsHistory); + const client = () => db.client(); + + async function findOne(table: Table, userId: string, hostId: number) { + const drizzle = await client(); + const rows = await drizzle + .select() + .from(table) + .where(and(eq(table.userId, userId), eq(table.hostId, hostId))) + .limit(1); + return rows[0] ?? null; + } + + async function upsert( + table: Table, + userId: string, + hostId: number, + values: Record, + ) { + const drizzle = await client(); + const now = sqlTimestamp(new Date()); + const existing = await findOne(table, userId, hostId); + if (existing) { + await drizzle + .update(table) + .set({ ...values, updatedAt: now }) + .where(eq(table.id, existing.id)); + } else { + await drizzle.insert(table).values({ + ...values, + userId, + hostId, + createdAt: now, + updatedAt: now, + }); + } + await db.persist(); + } + + return { + async findLayout(userId: string, hostId: number): Promise { + return (await findOne(preferences, userId, hostId))?.layout ?? null; + }, + + saveLayout(userId: string, hostId: number, layout: string) { + return upsert(preferences, userId, hostId, { layout }); + }, + + async findChecks( + userId: string, + hostId: number, + ): Promise<{ checks: string; intervalSeconds: number } | null> { + return findOne(checks, userId, hostId); + }, + + saveChecks( + userId: string, + hostId: number, + json: string, + intervalSeconds: number, + ) { + return upsert(checks, userId, hostId, { checks: json, intervalSeconds }); + }, + + /** Adds results and keeps only the newest `keep` rows for the host. */ + async recordHealth( + userId: string, + hostId: number, + results: HealthResultInput[], + keep: number, + ): Promise { + if (results.length === 0) return; + const drizzle = await client(); + const ts = sqlTimestamp(new Date()); + await drizzle + .insert(health) + .values(results.map((result) => ({ ...result, userId, hostId, ts }))); + const scope = and(eq(health.userId, userId), eq(health.hostId, hostId)); + const retained = await drizzle + .select({ id: health.id }) + .from(health) + .where(scope) + .orderBy(desc(health.ts), desc(health.id)) + .limit(keep); + await drizzle.delete(health).where( + retained.length + ? and( + scope, + notInArray( + health.id, + retained.map((row: { id: number }) => row.id), + ), + ) + : scope, + ); + await db.persist(); + }, + + async listHealth( + userId: string, + hostId: number, + limit: number, + ): Promise { + const drizzle = await client(); + const rows = await drizzle + .select() + .from(health) + .where(and(eq(health.userId, userId), eq(health.hostId, hostId))) + .orderBy(desc(health.ts), desc(health.id)) + .limit(limit); + return rows.map((row: Record) => ({ + checkId: row.checkId as string, + ts: row.ts as string, + ok: row.ok === true || row.ok === 1, + latencyMs: (row.latencyMs as number | null) ?? null, + detail: (row.detail as string | null) ?? null, + })); + }, + + /** One sample, then drops the host's samples older than the retention. */ + async recordSample( + input: MetricsHistoryInput, + retentionDays: number, + ): Promise { + const drizzle = await client(); + const now = new Date(); + await drizzle.insert(history).values({ ...input, ts: sqlTimestamp(now) }); + const cutoff = new Date(now.getTime() - retentionDays * 86_400_000); + await drizzle + .delete(history) + .where( + and( + eq(history.hostId, input.hostId), + lt(history.ts, sqlTimestamp(cutoff)), + ), + ); + // A sample every few seconds per host: let the debounced save pick it up. + await db.persist({ lazy: true }); + }, + + async listSamples( + hostId: number, + fromTs: string, + toTs: string, + ): Promise { + const drizzle = await client(); + return drizzle + .select({ + ts: history.ts, + cpuPercent: history.cpuPercent, + memPercent: history.memPercent, + diskPercent: history.diskPercent, + netRxBytes: history.netRxBytes, + netTxBytes: history.netTxBytes, + }) + .from(history) + .where( + and( + eq(history.hostId, hostId), + gte(history.ts, fromTs), + lte(history.ts, toTs), + ), + ) + .orderBy(asc(history.ts)); + }, + }; +} diff --git a/plugins/host-metrics/src/backend/routes.ts b/plugins/host-metrics/src/backend/routes.ts index 44422d5fc..109825f1f 100644 --- a/plugins/host-metrics/src/backend/routes.ts +++ b/plugins/host-metrics/src/backend/routes.ts @@ -1,1054 +1,49 @@ -import { getErrorMessage } from "../../../../src/backend/utils/error-message.js"; -import express, { type Router } from "express"; -import net from "net"; -import { Client, type ConnectConfig } from "ssh2"; -import { - getCurrentSettingValue, - createCurrentHostMetricsHistoryRepository, - createCurrentHostResolutionRepository, -} from "../../../../src/backend/database/repositories/factory.js"; -import { statsLogger } from "../../../../src/backend/utils/logger.js"; -import { DataCrypto } from "../../../../src/backend/utils/data-crypto.js"; -import { AuthManager } from "../../../../src/backend/utils/auth-manager.js"; -import { PermissionManager } from "../../../../src/backend/utils/permission-manager.js"; import type { - AuthenticatedRequest, - ProxyNode, -} from "../../../../src/types/index.js"; -import type { - LogEntry, - ConnectionStage, -} from "../../../../src/types/connection-log.js"; + NextFunction, + Request, + RequestHandler, + Response, + Router, +} from "express"; +import type { PluginContext } from "@termix/plugin-sdk/backend"; import { - collectCpuMetrics, - clearCpuSampleCache, -} from "./widgets/cpu-collector.js"; -import { collectMemoryMetrics } from "./widgets/memory-collector.js"; + defaultLayoutFromWidgets, + deriveEnabledWidgets, + type HostMetricsLayout, +} from "../shared/host-metrics.js"; +import { newSessionId, supportsMetrics } from "./helpers.js"; import { - collectDiskMetrics, - type DiskFilesystem, -} from "./widgets/disk-collector.js"; -import { - collectNetworkMetrics, - clearNetworkSampleCache, -} from "./widgets/network-collector.js"; -import { collectUptimeMetrics } from "./widgets/uptime-collector.js"; -import { collectProcessesMetrics } from "./widgets/processes-collector.js"; -import { collectSystemMetrics } from "./widgets/system-collector.js"; -import { detectPlatform } from "../../../../src/backend/hosts/metrics-shared/common-utils.js"; -import { collectLoginStats } from "./widgets/login-stats-collector.js"; -import { collectPortsMetrics } from "./widgets/ports-collector.js"; -import { collectFirewallMetrics } from "./widgets/firewall-collector.js"; -import { collectTemperatureMetrics } from "./widgets/temperature-collector.js"; -import { collectGpuMetrics, type GpuMetrics } from "./widgets/gpu-collector.js"; -import { connectionPool } from "../../../../src/backend/hosts/ssh-connection-pool.js"; -import { registerHostMetricsSettingsRoutes } from "./settings-routes.js"; -import { registerHostMetricsViewerRoutes } from "./viewer-routes.js"; -import { registerHostMetricsPreferencesRoutes } from "./preferences-routes.js"; -import { registerHostMetricsHistoryRoutes } from "./history-routes.js"; -import type { HostSessionStatusPayload } from "../../../../src/backend/hosts/host-session-status.js"; -import { - pluginEvents, - TOPICS, -} from "../../../../src/backend/plugins/events.js"; -import { - notifyAutomationMetrics, - notifyAutomationStatus, -} from "./automation-bridge.js"; -import { notifyAutomationInternalEvent } from "../../../../src/backend/hosts/automation-events.js"; + startInteractive, + submitTotp, + type InteractiveDeps, +} from "./interactive.js"; +import type { MetricsLogger } from "./log.js"; import { registerManagerRoutes } from "./managers/index.js"; import { AccessDeniedError } from "./managers/route-helpers.js"; -import type { ManagerHost } from "./managers/types.js"; -import type { PluginSshHost } from "@termix/plugin-sdk/backend"; -import { pluginSsh, withSshConnection as withPooledSsh } from "./ssh.js"; -import { resolveHostById } from "../../../../src/backend/hosts/host-resolver.js"; -import { - isTcpPingEnabled, - parseStatusHostIds, - supportsMetrics, - tcpPingThroughJumpHost, -} from "./helpers.js"; -import { - type HostStatus, - isHostKeyVerificationError, - statusAfterAuthentication, - statusAfterReachabilityCheck, -} from "../../../../src/backend/hosts/metrics-shared/host-status.js"; -import { createConnectionLog } from "../../../../src/backend/hosts/connection-log.js"; -import { - cleanupMetricsSession, - getSessionKey, - metricsSessions, - pendingTOTPSessions, - scheduleMetricsSessionCleanup, - type MetricsViewer, -} from "./sessions.js"; -import { - authFailureTracker, - canStartInitialMetrics, - hostPollCache, - initialMetricsPollLimiter, - metricsCache, - metricsPollLimiter, - metricsConcurrencyFor, - pollingBackoff, - requestQueue, - statusPollLimiter, -} from "../../../../src/backend/hosts/metrics-shared/state.js"; +import type { HealthCheckEvent } from "./managers/types.js"; +import type { MetricsPoller } from "./poller.js"; +import { sqlTimestamp, type HostMetricsRepository } from "./repository.js"; +import { sessionKey, type MetricsSessions } from "./sessions.js"; +import { sudoPasswordOf } from "./helpers.js"; -const authManager = AuthManager.getInstance(); -const permissionManager = PermissionManager.getInstance(); - -interface SSHHostWithCredentials { - id: number; - name: string; - ip: string; - port: number; - username: string; - folder: string; - tags: string[]; - pin: boolean; - authType: string; - password?: string; - key?: string; - keyPassword?: string; - keyType?: string; - sudoPassword?: string; - credentialId?: number; - enableTerminal: boolean; - enableTunnel: boolean; - enableFileManager: boolean; - defaultPath: string; - tunnelConnections: unknown[]; - jumpHosts?: Array<{ hostId: number }>; - statsConfig?: string | StatsConfig; - createdAt: string; - updatedAt: string; - userId: string; - - useSocks5?: boolean; - socks5Host?: string; - socks5Port?: number; - socks5Username?: string; - socks5Password?: string; - socks5ProxyChain?: ProxyNode[]; - connectionType?: "ssh" | "rdp" | "vnc" | "telnet"; - rdpPort?: number; - vncPort?: number; - telnetPort?: number; - vaultProfile?: { id?: number | null } | null; +export interface RouteDeps { + ctx: PluginContext; + log: MetricsLogger; + poller: MetricsPoller; + sessions: MetricsSessions; + repository: HostMetricsRepository; + onHealthCheck: (event: HealthCheckEvent) => void; } -type StatusEntry = { - status: HostStatus; - lastChecked: string; - reason?: "host_key_changed"; +const RANGE_OFFSETS: Record = { + "1h": 60 * 60 * 1000, + "6h": 6 * 60 * 60 * 1000, + "24h": 24 * 60 * 60 * 1000, + "7d": 7 * 24 * 60 * 60 * 1000, + "30d": 30 * 24 * 60 * 60 * 1000, }; -interface StatsConfig { - enabledWidgets: string[]; - statusCheckEnabled: boolean; - statusCheckInterval: number; - useGlobalStatusInterval?: boolean; - metricsEnabled: boolean; - metricsInterval: number; - useGlobalMetricsInterval?: boolean; - disableTcpPing?: boolean; - excludedMounts?: string[]; - monitoredMounts?: Array<{ path: string; label?: string }>; -} - -const DEFAULT_STATS_CONFIG: StatsConfig = { - enabledWidgets: [ - "cpu", - "memory", - "disk", - "network", - "uptime", - "system", - "login_stats", - "processes", - "ports", - "firewall", - "temperature", - ], - statusCheckEnabled: true, - statusCheckInterval: 60, - metricsEnabled: true, - metricsInterval: 30, - excludedMounts: [], -}; - -interface HostPollingConfig { - host: SSHHostWithCredentials; - statsConfig: StatsConfig; - statusTimer?: NodeJS.Timeout; - metricsTimer?: NodeJS.Timeout; - viewerUserId?: string; -} - -class PollingManager { - private pollingConfigs = new Map(); - private statusStore = new Map(); - private metricsStore = new Map< - number, - { - data: Awaited>; - timestamp: number; - } - >(); - private activeViewers = new Map>(); - private viewerDetails = new Map(); - private viewerCleanupInterval: NodeJS.Timeout; - /** Skip stacking another status/metrics poll while one is already running. */ - private statusInFlight = new Set(); - private metricsInFlight = new Set(); - private initialMetricsRequested = new Set(); - private terminalOnlineHosts = new Set(); - private metricsAuthenticatedHosts = new Set(); - private unsubscribeHostSessionStatus: () => void; - - constructor() { - this.unsubscribeHostSessionStatus = pluginEvents.on( - TOPICS.hostSessionStatus, - (payload) => { - const { hostId, online } = payload as HostSessionStatusPayload; - this.setTerminalSessionOnline(hostId, online); - }, - ); - this.viewerCleanupInterval = setInterval(() => { - this.cleanupInactiveViewers(); - }, 60000); - } - - private setTerminalSessionOnline(hostId: number, online: boolean): void { - if (online) { - this.terminalOnlineHosts.add(hostId); - const config = this.pollingConfigs.get(hostId); - if (config && isTcpPingEnabled(config.statsConfig)) { - this.statusStore.set(hostId, { - status: "online", - lastChecked: new Date().toISOString(), - }); - } - return; - } - - this.terminalOnlineHosts.delete(hostId); - if ( - !this.metricsAuthenticatedHosts.has(hostId) && - this.statusStore.get(hostId)?.status === "online" - ) { - this.statusStore.set(hostId, { - status: "reachable", - lastChecked: new Date().toISOString(), - }); - } - } - - /** - * Keeps poll concurrency matched to how many hosts are actually being - * polled, so a sweep still finishes inside its interval as a fleet grows. - */ - private syncPollConcurrency(): void { - const metricsHosts = Array.from(this.pollingConfigs.values()).filter( - (config) => config.statsConfig.metricsEnabled, - ).length; - - const target = metricsConcurrencyFor(metricsHosts); - if (target === metricsPollLimiter.limit) return; - - const previous = metricsPollLimiter.limit; - metricsPollLimiter.setLimit(target); - statsLogger.info( - `Metrics poll concurrency ${previous} -> ${target} for ${metricsHosts} host(s)`, - { - operation: "metrics_concurrency_resize", - hosts: metricsHosts, - concurrency: target, - }, - ); - } - - /** Spread timers so N hosts do not fire on the same wall-clock second. */ - private intervalWithJitter(intervalMs: number, hostId: number): number { - const spread = Math.min(intervalMs * 0.2, 15_000); - const jitter = (hostId * 1103515245) % Math.max(1, Math.floor(spread)); - return intervalMs + jitter; - } - - private async resolveHostForPoll( - host: SSHHostWithCredentials, - userId: string, - ): Promise { - const cached = hostPollCache.get( - host.id, - userId, - ) as SSHHostWithCredentials | null; - if (cached) { - return cached; - } - - const refreshed = await fetchHostById(host.id, userId); - if (!refreshed) { - return null; - } - - hostPollCache.set(host.id, userId, refreshed); - const config = this.pollingConfigs.get(host.id); - if (config) { - config.host = refreshed; - config.statsConfig = this.parseStatsConfig(refreshed.statsConfig); - } - return refreshed; - } - - private scheduleStatusPoll( - host: SSHHostWithCredentials, - viewerUserId?: string, - ): void { - if (this.statusInFlight.has(host.id)) { - return; - } - this.statusInFlight.add(host.id); - void statusPollLimiter - .run(() => this.pollHostStatus(host, viewerUserId)) - .catch((err) => { - statsLogger.error("Status polling failed", err, { - operation: "status_poll_unhandled", - hostId: host.id, - }); - }) - .finally(() => { - this.statusInFlight.delete(host.id); - }); - } - - private scheduleMetricsPoll( - host: SSHHostWithCredentials, - viewerUserId?: string, - ): void { - if (this.metricsInFlight.has(host.id)) { - return; - } - this.metricsInFlight.add(host.id); - void metricsPollLimiter - .run(() => this.pollHostMetrics(host, viewerUserId)) - .catch((err) => { - statsLogger.error("Metrics polling failed", err, { - operation: "metrics_poll_unhandled", - hostId: host.id, - }); - }) - .finally(() => { - this.metricsInFlight.delete(host.id); - }); - } - - private scheduleInitialMetricsPoll( - host: SSHHostWithCredentials, - viewerUserId?: string, - ): void { - if ( - this.metricsStore.has(host.id) || - this.initialMetricsRequested.has(host.id) - ) { - return; - } - - this.initialMetricsRequested.add(host.id); - void initialMetricsPollLimiter - .run(async () => { - if ( - !canStartInitialMetrics( - this.statusStore.get(host.id)?.status, - this.activeViewers.has(host.id), - isTcpPingEnabled( - this.pollingConfigs.get(host.id)?.statsConfig ?? - this.parseStatsConfig(host.statsConfig), - ), - ) - ) { - return; - } - if (this.metricsInFlight.has(host.id)) return; - - this.metricsInFlight.add(host.id); - try { - await metricsPollLimiter.run(() => - this.pollHostMetrics(host, viewerUserId), - ); - } finally { - this.metricsInFlight.delete(host.id); - } - }) - .catch((err) => { - statsLogger.error("Initial metrics polling failed", err, { - operation: "initial_metrics_poll_unhandled", - hostId: host.id, - }); - }) - .finally(() => { - this.initialMetricsRequested.delete(host.id); - }); - } - - private getGlobalDefaults(): { - statusCheckInterval: number; - metricsInterval: number; - } { - try { - const statusValue = getCurrentSettingValue( - "global_status_check_interval", - ); - const metricsValue = getCurrentSettingValue("global_metrics_interval"); - - return { - statusCheckInterval: statusValue - ? parseInt(statusValue, 10) || - DEFAULT_STATS_CONFIG.statusCheckInterval - : DEFAULT_STATS_CONFIG.statusCheckInterval, - metricsInterval: metricsValue - ? parseInt(metricsValue, 10) || DEFAULT_STATS_CONFIG.metricsInterval - : DEFAULT_STATS_CONFIG.metricsInterval, - }; - } catch { - return { - statusCheckInterval: DEFAULT_STATS_CONFIG.statusCheckInterval, - metricsInterval: DEFAULT_STATS_CONFIG.metricsInterval, - }; - } - } - - parseStatsConfig(statsConfigStr?: string | StatsConfig): StatsConfig { - if (!statsConfigStr) { - return DEFAULT_STATS_CONFIG; - } - - let parsed: StatsConfig; - - if (typeof statsConfigStr === "object") { - parsed = statsConfigStr; - } else { - try { - let temp: unknown = JSON.parse(statsConfigStr); - - if (typeof temp === "string") { - temp = JSON.parse(temp); - } - - parsed = temp as StatsConfig; - } catch (error) { - statsLogger.warn( - `Failed to parse statsConfig: ${getErrorMessage(error)}`, - { - operation: "parse_stats_config_error", - statsConfigStr, - }, - ); - return DEFAULT_STATS_CONFIG; - } - } - - const result = { ...DEFAULT_STATS_CONFIG, ...parsed }; - - const globalDefaults = this.getGlobalDefaults(); - if (result.useGlobalStatusInterval !== false) { - result.statusCheckInterval = globalDefaults.statusCheckInterval; - } - if (result.useGlobalMetricsInterval !== false) { - result.metricsInterval = globalDefaults.metricsInterval; - } - - return result; - } - - async startPollingForHost( - host: SSHHostWithCredentials, - options?: { statusOnly?: boolean; viewerUserId?: string }, - ): Promise { - const statsConfig = this.parseStatsConfig(host.statsConfig); - const statusOnly = options?.statusOnly ?? false; - const viewerUserId = options?.viewerUserId; - - const canCollectMetrics = supportsMetrics(host); - - const enabledCollectors: string[] = []; - if (isTcpPingEnabled(statsConfig)) { - enabledCollectors.push("status"); - } - if (!statusOnly && statsConfig.metricsEnabled && canCollectMetrics) { - enabledCollectors.push( - "cpu", - "memory", - "disk", - "network", - "uptime", - "processes", - "system", - ); - } - - const existingConfig = this.pollingConfigs.get(host.id); - - if (existingConfig) { - if (existingConfig.statusTimer) { - clearInterval(existingConfig.statusTimer); - existingConfig.statusTimer = undefined; - } - if (existingConfig.metricsTimer) { - clearInterval(existingConfig.metricsTimer); - existingConfig.metricsTimer = undefined; - } - } - - if (!isTcpPingEnabled(statsConfig) && !statsConfig.metricsEnabled) { - this.pollingConfigs.delete(host.id); - this.syncPollConcurrency(); - this.statusStore.delete(host.id); - this.metricsStore.delete(host.id); - clearCpuSampleCache(host.id); - clearNetworkSampleCache(host.id); - return; - } - - const config: HostPollingConfig = { - host, - statsConfig, - viewerUserId, - }; - this.pollingConfigs.set(host.id, config); - - if (isTcpPingEnabled(statsConfig)) { - const intervalMs = this.intervalWithJitter( - statsConfig.statusCheckInterval * 1000, - host.id, - ); - - this.scheduleStatusPoll(host, viewerUserId); - - config.statusTimer = setInterval(() => { - const latestConfig = this.pollingConfigs.get(host.id); - if (latestConfig && isTcpPingEnabled(latestConfig.statsConfig)) { - this.scheduleStatusPoll(latestConfig.host, latestConfig.viewerUserId); - } - }, intervalMs); - } else { - this.statusStore.delete(host.id); - } - - if (!statusOnly && statsConfig.metricsEnabled && canCollectMetrics) { - const intervalMs = this.intervalWithJitter( - statsConfig.metricsInterval * 1000, - host.id, - ); - - // Viewer registration can arrive for an entire fleet at once. Only - // collect the first heavy SSH sample after the cheap status probe has - // confirmed the host is reachable; the status completion path below - // starts it when the result was not already cached. - if ( - canStartInitialMetrics( - this.statusStore.get(host.id)?.status, - this.activeViewers.has(host.id), - isTcpPingEnabled(statsConfig), - ) - ) { - this.scheduleInitialMetricsPoll(host, viewerUserId); - } - - config.metricsTimer = setInterval(() => { - const latestConfig = this.pollingConfigs.get(host.id); - if ( - latestConfig && - latestConfig.statsConfig.metricsEnabled && - supportsMetrics(latestConfig.host) - ) { - this.scheduleMetricsPoll( - latestConfig.host, - latestConfig.viewerUserId, - ); - } - }, intervalMs); - } else { - this.metricsStore.delete(host.id); - } - - this.syncPollConcurrency(); - } - - private async pollHostStatus( - host: SSHHostWithCredentials, - viewerUserId?: string, - ): Promise { - const userId = viewerUserId || host.userId; - const refreshedHost = await this.resolveHostForPoll(host, userId); - if (!refreshedHost) { - return; - } - - try { - const ct = refreshedHost.connectionType || "ssh"; - let pingPort: number; - if (ct === "rdp") pingPort = refreshedHost.rdpPort ?? 3389; - else if (ct === "vnc") pingPort = refreshedHost.vncPort ?? 5900; - else if (ct === "telnet") pingPort = refreshedHost.telnetPort ?? 23; - else pingPort = refreshedHost.port; - - let isOnline: boolean; - if (refreshedHost.jumpHosts && refreshedHost.jumpHosts.length > 0) { - let jumpClient: Client | null = null; - try { - ({ client: jumpClient } = await pluginSsh().jumpChain( - refreshedHost.jumpHosts, - { forHost: refreshedHost as unknown as PluginSshHost }, - )); - } catch { - jumpClient = null; - } - isOnline = jumpClient - ? await tcpPingThroughJumpHost( - jumpClient, - refreshedHost.ip, - pingPort, - 5000, - ) - : false; - } else { - isOnline = await tcpPing(refreshedHost.ip, pingPort, 5000); - } - const config = this.pollingConfigs.get(refreshedHost.id); - // Routine status polling is TCP-only. Hosts with metrics disabled - // never get a full SSH auth attempt here, since on RADIUS/Duo-backed - // devices that fires a live 2FA push every interval with no session - // to show for it; those hosts settle at "reachable" rather than - // "online", since metrics collection (the only path that - // authenticates and can promote a host to "online") never runs for - // them. - const statusEntry: StatusEntry = { - status: - isOnline && this.terminalOnlineHosts.has(refreshedHost.id) - ? "online" - : statusAfterReachabilityCheck( - isOnline, - this.statusStore.get(refreshedHost.id)?.status, - ), - lastChecked: new Date().toISOString(), - ...(isOnline && - this.statusStore.get(refreshedHost.id)?.reason === "host_key_changed" - ? { reason: "host_key_changed" as const } - : {}), - }; - this.statusStore.set(refreshedHost.id, statusEntry); - if (isOnline && this.activeViewers.has(refreshedHost.id)) { - if (config?.statsConfig.metricsEnabled) { - this.scheduleInitialMetricsPoll(config.host, config.viewerUserId); - } - } - notifyAutomationStatus(refreshedHost.id, refreshedHost.userId, isOnline); - } catch { - const statusEntry: StatusEntry = { - status: "offline", - lastChecked: new Date().toISOString(), - }; - this.statusStore.set(refreshedHost.id, statusEntry); - notifyAutomationStatus(refreshedHost.id, refreshedHost.userId, false); - } - } - - private async pollHostMetrics( - host: SSHHostWithCredentials, - viewerUserId?: string, - ): Promise { - const userId = viewerUserId || host.userId; - const refreshedHost = await this.resolveHostForPoll(host, userId); - if (!refreshedHost) { - return; - } - - if (!supportsMetrics(refreshedHost)) { - statsLogger.debug("Skipping metrics collection for non-SSH host", { - operation: "poll_host_metrics_skipped", - hostId: refreshedHost.id, - connectionType: refreshedHost.connectionType || "ssh", - }); - return; - } - - const config = this.pollingConfigs.get(refreshedHost.id); - if (!config || !config.statsConfig.metricsEnabled) { - return; - } - - if (authFailureTracker.shouldSkip(host.id)) { - return; - } - - if (pollingBackoff.shouldSkip(host.id)) { - return; - } - - let authenticated = false; - try { - const metrics = await collectMetrics(refreshedHost, () => { - authenticated = true; - this.metricsAuthenticatedHosts.add(refreshedHost.id); - this.statusStore.set(refreshedHost.id, { - status: statusAfterAuthentication(true), - lastChecked: new Date().toISOString(), - }); - }); - this.statusStore.set(refreshedHost.id, { - status: statusAfterAuthentication(true), - lastChecked: new Date().toISOString(), - }); - this.metricsStore.set(refreshedHost.id, { - data: metrics, - timestamp: Date.now(), - }); - await this.insertMetricsHistory(refreshedHost.id, metrics); - notifyAutomationMetrics(refreshedHost.id, refreshedHost.userId, metrics); - pollingBackoff.reset(refreshedHost.id); - authFailureTracker.reset(refreshedHost.id); - } catch (error) { - const hostKeyChanged = isHostKeyVerificationError(error); - if (!authenticated) { - this.metricsAuthenticatedHosts.delete(refreshedHost.id); - this.statusStore.set(refreshedHost.id, { - status: this.terminalOnlineHosts.has(refreshedHost.id) - ? "online" - : statusAfterAuthentication( - false, - this.statusStore.get(refreshedHost.id)?.status, - ), - lastChecked: new Date().toISOString(), - ...(hostKeyChanged ? { reason: "host_key_changed" as const } : {}), - }); - } - const isAuthError = - error instanceof Error && - (error.message.includes("authentication") || - error.message.includes("Authentication") || - error.message.includes("permission denied") || - error.message.includes("Permission denied")); - - if (isAuthError) { - // authFailureTracker already handles auth errors inside collectMetrics; - // only log on the first occurrence to avoid repeated spam - const alreadyTracked = authFailureTracker.shouldSkip(host.id); - if (!alreadyTracked) { - statsLogger.error("Stats collector connection failed", error, { - operation: "stats_connect_failed", - hostId: refreshedHost.id, - }); - } - return; - } - - if (hostKeyChanged) { - authFailureTracker.recordFailure(refreshedHost.id, "HOST_KEY", true); - statsLogger.error( - "Stats collector host key verification failed", - error, - { - operation: "stats_host_key_verification_failed", - hostId: refreshedHost.id, - }, - ); - return; - } - - pollingBackoff.recordFailure(refreshedHost.id); - - // Only log when a new retry window opens, not on every skipped poll - const backoff = pollingBackoff.getBackoffInfo(refreshedHost.id); - const isNewFailure = - backoff !== null && !backoff.includes("polling suspended"); - if (isNewFailure) { - statsLogger.error("Stats collector connection failed", error, { - operation: "stats_connect_failed", - hostId: refreshedHost.id, - }); - } - } - } - - private getRetentionDays(): number { - try { - const value = getCurrentSettingValue("metrics_history_retention_days"); - const days = value ? parseInt(value, 10) : 7; - return isNaN(days) || days < 1 ? 7 : Math.min(days, 90); - } catch { - return 7; - } - } - - private async insertMetricsHistory( - hostId: number, - metrics: { - cpu: { percent: number | null }; - memory: { percent: number | null }; - disk: { percent: number | null }; - network: { - interfaces: Array<{ rxBytes: string | null; txBytes: string | null }>; - }; - }, - ): Promise { - try { - const iface = metrics.network?.interfaces?.[0]; - const rxRaw = iface?.rxBytes ? parseInt(iface.rxBytes, 10) : null; - const txRaw = iface?.txBytes ? parseInt(iface.txBytes, 10) : null; - - const repository = createCurrentHostMetricsHistoryRepository(); - await repository.create({ - hostId, - cpuPercent: metrics.cpu?.percent ?? null, - memPercent: metrics.memory?.percent ?? null, - diskPercent: metrics.disk?.percent ?? null, - netRxBytes: isNaN(rxRaw as number) ? null : rxRaw, - netTxBytes: isNaN(txRaw as number) ? null : txRaw, - }); - - const retentionDays = this.getRetentionDays(); - await repository.pruneOlderThan(hostId, retentionDays); - } catch (err) { - statsLogger.warn("Failed to write metrics history", { - operation: "insert_metrics_history", - hostId, - error: err instanceof Error ? err.message : String(err), - }); - } - } - - stopPollingForHost(hostId: number, clearData = true): void { - const config = this.pollingConfigs.get(hostId); - if (config) { - if (config.statusTimer) { - clearInterval(config.statusTimer); - config.statusTimer = undefined; - } - if (config.metricsTimer) { - clearInterval(config.metricsTimer); - config.metricsTimer = undefined; - } - - this.pollingConfigs.delete(hostId); - - this.syncPollConcurrency(); - if (clearData) { - this.statusStore.delete(hostId); - this.metricsStore.delete(hostId); - clearCpuSampleCache(hostId); - clearNetworkSampleCache(hostId); - } - } - hostPollCache.invalidate(hostId); - this.statusInFlight.delete(hostId); - this.metricsInFlight.delete(hostId); - } - - stopMetricsOnly(hostId: number): void { - const config = this.pollingConfigs.get(hostId); - if (config?.metricsTimer) { - clearInterval(config.metricsTimer); - config.metricsTimer = undefined; - } - } - - getStatus(hostId: number): StatusEntry | undefined { - return this.statusStore.get(hostId); - } - - getAllStatuses(): Map { - return this.statusStore; - } - - getMetrics( - hostId: number, - ): - | { data: Awaited>; timestamp: number } - | undefined { - return this.metricsStore.get(hostId); - } - - async initializePolling(userId: string): Promise { - const hosts = await fetchAllHosts(userId); - - for (const host of hosts) { - await this.startPollingForHost(host, { statusOnly: true }); - } - } - - async reconcileStatusPolling( - userId: string, - allowedHostIds: Set, - ): Promise { - const hosts = await fetchAllHosts(userId); - - for (const host of hosts) { - if (allowedHostIds.has(host.id)) { - if (!this.pollingConfigs.has(host.id)) { - await this.startPollingForHost(host, { statusOnly: true }); - } - continue; - } - - this.stopPollingForHost(host.id, true); - } - } - - async refreshHostPolling(userId: string): Promise { - const hosts = await fetchAllHosts(userId); - const currentHostIds = new Set(hosts.map((h) => h.id)); - - for (const hostId of this.pollingConfigs.keys()) { - this.stopPollingForHost(hostId, false); - } - - for (const hostId of this.statusStore.keys()) { - if (!currentHostIds.has(hostId)) { - this.statusStore.delete(hostId); - this.metricsStore.delete(hostId); - clearCpuSampleCache(hostId); - clearNetworkSampleCache(hostId); - } - } - - for (const host of hosts) { - await this.startPollingForHost(host, { statusOnly: true }); - } - } - - async refreshAllPolling(): Promise { - const hostsToRefresh: Array<{ - host: SSHHostWithCredentials; - viewerUserId?: string; - }> = []; - - for (const [hostId, config] of this.pollingConfigs.entries()) { - const status = this.statusStore.get(hostId); - - if ( - !status || - status.status === "online" || - status.status === "reachable" - ) { - hostsToRefresh.push({ - host: config.host, - viewerUserId: config.viewerUserId, - }); - } - } - - for (const hostId of this.pollingConfigs.keys()) { - this.stopPollingForHost(hostId, false); - } - - for (const { host, viewerUserId } of hostsToRefresh) { - await this.startPollingForHost(host, { statusOnly: true, viewerUserId }); - } - } - - registerViewer(hostId: number, sessionId: string, userId: string): void { - if (!this.activeViewers.has(hostId)) { - this.activeViewers.set(hostId, new Set()); - } - this.activeViewers.get(hostId)!.add(sessionId); - - this.viewerDetails.set(sessionId, { - sessionId, - userId, - hostId, - lastHeartbeat: Date.now(), - }); - - if (this.activeViewers.get(hostId)!.size === 1) { - // Fire-and-forget: never let background metrics start-up failures - // propagate up to the HTTP handler that registered the viewer. - Promise.resolve() - .then(() => this.startMetricsForHost(hostId, userId)) - .catch((err) => { - statsLogger.warn("startMetricsForHost rejected (non-fatal)", { - operation: "start_metrics_unhandled", - hostId, - userId, - error: err instanceof Error ? err.message : String(err), - }); - }); - } - } - - updateHeartbeat(sessionId: string): boolean { - const viewer = this.viewerDetails.get(sessionId); - if (viewer) { - viewer.lastHeartbeat = Date.now(); - return true; - } - return false; - } - - unregisterViewer(hostId: number, sessionId: string): void { - const viewers = this.activeViewers.get(hostId); - if (viewers) { - viewers.delete(sessionId); - - if (viewers.size === 0) { - this.activeViewers.delete(hostId); - this.stopMetricsForHost(hostId); - } - } - this.viewerDetails.delete(sessionId); - } - - private async startMetricsForHost( - hostId: number, - userId: string, - ): Promise { - try { - const host = await fetchHostById(hostId, userId); - if (host) { - await this.startPollingForHost(host, { viewerUserId: userId }); - } - } catch (error) { - statsLogger.error("Failed to start metrics polling", { - operation: "start_metrics_error", - hostId, - error: error instanceof Error ? error.message : String(error), - }); - } - } - - private stopMetricsForHost(hostId: number): void { - this.stopMetricsOnly(hostId); - } - - private cleanupInactiveViewers(): void { - const now = Date.now(); - const maxInactivity = 120000; - - for (const [sessionId, viewer] of this.viewerDetails.entries()) { - if (now - viewer.lastHeartbeat > maxInactivity) { - this.unregisterViewer(viewer.hostId, sessionId); - } - } - } - - destroy(): void { - this.unsubscribeHostSessionStatus(); - clearInterval(this.viewerCleanupInterval); - for (const hostId of this.pollingConfigs.keys()) { - this.stopPollingForHost(hostId); - } - } -} - -const pollingManager = new PollingManager(); - -function validateHostId( - req: express.Request, - res: express.Response, - next: express.NextFunction, -) { +function validateHostId(req: Request, res: Response, next: NextFunction) { const id = Number(req.params.id); if (!id || !Number.isInteger(id) || id <= 0) { return res.status(400).json({ error: "Invalid host ID" }); @@ -1056,1459 +51,655 @@ function validateHostId( next(); } -// Mounted onto the router core hands this plugin at /plugin-api/host-metrics, -// rather than listening on a port of its own. Compression, CORS, cookies and -// body parsing all run in core's stack in front of this, so only the -// cache header and the routes themselves stay here. -export const app = express(); -app.set("trust proxy", "loopback"); -app.use((_req, res, next) => { - res.setHeader("Cache-Control", "no-store"); - next(); -}); - -// Core authenticates every /plugin-api request before it reaches this app. -const requireAdmin = authManager.createAdminMiddleware(); - -async function fetchAllHosts( - userId: string, -): Promise { - try { - const repository = createCurrentHostResolutionRepository(); - const hostResults = await repository.findHostsByUserId(userId); - - const hostsWithCredentials: SSHHostWithCredentials[] = []; - for (const host of hostResults) { - try { - const hostWithCreds = await resolveHostCredentials(host, userId); - if (hostWithCreds) { - hostsWithCredentials.push(hostWithCreds); - } - } catch (err) { - statsLogger.warn( - `Failed to resolve credentials for host ${host.id}: ${getErrorMessage(err)}`, - ); - } - } - - return hostsWithCredentials.filter((h) => !!h.id && !!h.ip && !!h.port); - } catch (err) { - statsLogger.error("Failed to fetch hosts from database", err); - return []; - } +export function sanitizeLayout(input: unknown): HostMetricsLayout | null { + if (!input || typeof input !== "object") return null; + const obj = input as Record; + if (!Array.isArray(obj.slots)) return null; + const columns = + typeof obj.columns === "number" && obj.columns >= 1 && obj.columns <= 4 + ? Math.round(obj.columns) + : 3; + const slots = obj.slots + .filter((s): s is Record => !!s && typeof s === "object") + .map((s, i) => ({ + id: String(s.id), + order: typeof s.order === "number" ? s.order : i, + colSpan: + s.colSpan === 1 || s.colSpan === 2 || s.colSpan === 3 ? s.colSpan : 1, + height: + typeof s.height === "number" && s.height > 0 + ? Math.round(s.height) + : null, + })) + .filter((s) => s.id && s.id !== "undefined"); + return { slots, columns } as HostMetricsLayout; } -async function fetchHostById( - id: number, - userId: string, -): Promise { - try { - if (DataCrypto.getUserDataKey(userId) === null) { - return undefined; - } +/** Everything under /plugin-api/host-metrics. Core authenticates first. */ +export function registerRoutes(router: Router, deps: RouteDeps): void { + const { ctx, log, poller, sessions, repository } = deps; - const host = await resolveHostById(id, userId); - if (!host) { - statsLogger.warn(`User ${userId} cannot resolve host ${id}`, { - operation: "fetch_host_access_denied", - userId, + router.use(ctx.rbac.require("use") as unknown as RequestHandler); + router.use((_req, res, next) => { + res.setHeader("Cache-Control", "no-store"); + next(); + }); + + const actor = (res: Response): string | null => { + const userId = ctx.currentActor(); + if (!userId) res.status(401).json({ error: "Authentication required" }); + return userId ?? null; + }; + + const canConnect = async (hostId: number) => + (await ctx.hosts.checkAccess(hostId, "connect")).hasAccess; + + const interactive: InteractiveDeps = { + ssh: ctx.ssh, + sessions, + log, + registerViewer: (hostId, sessionId, userId) => + poller.registerViewer(hostId, sessionId, userId), + }; + + /** + * @openapi + * /plugin-api/host-metrics/defaults: + * get: + * summary: Get defaults for new hosts + * description: Whether a new host starts with metrics on, an admin setting any user creating a host needs. + * tags: + * - Host Metrics + * responses: + * 200: + * description: The defaults. + */ + router.get("/defaults", async (_req, res) => { + res.json({ + enabledForNewHosts: + (await ctx.settings.get("enabledForNewHosts")) !== false, + }); + }); + + /** + * @openapi + * /plugin-api/host-metrics/metrics/{id}: + * get: + * summary: Get host metrics + * description: Returns the latest sample for a host, including CPU, memory, disk, network, processes and system information. + * tags: + * - Host Metrics + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: Host metrics data. + * 403: + * description: No access to this host. + * 404: + * description: Metrics not available. + */ + router.get("/metrics/:id", validateHostId, async (req, res) => { + const id = Number(req.params.id); + if (!actor(res)) return; + if (!(await canConnect(id))) { + return res.status(403).json({ error: "No access to this host" }); + } + const sample = poller.getMetrics(id); + if (!sample) { + return res.status(404).json({ + error: "Metrics not available", + cpu: { percent: null, cores: null, load: null }, + memory: { percent: null, usedGiB: null, totalGiB: null }, + disk: { + percent: null, + usedHuman: null, + totalHuman: null, + availableHuman: null, + mount: null, + filesystems: [], + }, + network: { interfaces: [] }, + uptime: { seconds: null, formatted: null }, + processes: { total: null, running: null, top: [] }, + system: { hostname: null, kernel: null, os: null }, + lastChecked: new Date().toISOString(), + }); + } + res.json({ + ...sample.data, + lastChecked: new Date(sample.timestamp).toISOString(), + }); + }); + + /** + * @openapi + * /plugin-api/host-metrics/metrics/start/{id}: + * post: + * summary: Start metrics collection + * description: Opens an SSH connection from the tab, where a TOTP prompt can be answered, and starts collecting metrics for the host. + * tags: + * - Host Metrics + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: Metrics collection started, or a TOTP code is required. + * 401: + * description: The host needs a sign-in step first. + * 404: + * description: Host not found. + * 500: + * description: Failed to start metrics collection. + */ + router.post("/metrics/start/:id", validateHostId, async (req, res) => { + const id = Number(req.params.id); + const userId = actor(res); + if (!userId) return; + try { + const host = await poller.resolve(id, userId); + if (!host) { + return res.status(404).json({ + error: "Host not found", + connectionLogs: [ + { + type: "error", + stage: "stats_connecting", + message: "Host not found", + }, + ], + }); + } + const result = await startInteractive(interactive, host, userId); + res.status(result.status).json(result.body); + } catch (error) { + log.error("Failed to start metrics collection", error, { + operation: "metrics_start_error", hostId: id, }); - return undefined; + res.status(500).json({ error: "Failed to start metrics collection" }); } + }); - return host as SSHHostWithCredentials; - } catch (err) { - statsLogger.error(`Failed to fetch host ${id}`, err); - return undefined; - } -} + /** + * @openapi + * /plugin-api/host-metrics/metrics/stop/{id}: + * post: + * summary: Stop metrics collection + * description: Closes the tab's connection and drops its viewer, which stops polling when it was the last one. + * tags: + * - Host Metrics + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * requestBody: + * content: + * application/json: + * schema: + * type: object + * properties: + * viewerSessionId: + * type: string + * responses: + * 200: + * description: Metrics collection stopped. + */ + router.post("/metrics/stop/:id", validateHostId, async (req, res) => { + const id = Number(req.params.id); + const userId = actor(res); + if (!userId) return; + sessions.close(sessionKey(id, userId)); + const { viewerSessionId } = (req.body ?? {}) as { + viewerSessionId?: unknown; + }; + if (typeof viewerSessionId === "string" && viewerSessionId) { + poller.unregisterViewer(id, viewerSessionId, userId); + } else if (await canConnect(id)) { + poller.pause(id); + } + res.json({ success: true }); + }); -/** - * Resolves through core's host resolver, so shared-host overrides and - * external secret references apply here exactly as in the terminal. - */ -async function resolveHostCredentials( - host: Record, - userId: string, -): Promise { - try { - const resolved = (await resolveHostById( - host.id as number, - userId, - )) as unknown as Record | null; - if (!resolved) return undefined; - const terminalConfig = resolved.terminalConfig as - Record | undefined; - return { - ...resolved, - tags: - typeof resolved.tags === "string" - ? resolved.tags - ? resolved.tags.split(",").filter(Boolean) - : [] - : (resolved.tags ?? []), - sudoPassword: - (resolved.sudoPassword as string | undefined) || - (terminalConfig?.sudoPassword as string | undefined) || - undefined, - connectionType: - (resolved.connectionType as SSHHostWithCredentials["connectionType"]) || - "ssh", - } as unknown as SSHHostWithCredentials; - } catch (error) { - statsLogger.error( - `Failed to resolve host credentials for host ${host.id}: ${getErrorMessage(error)}`, - ); - return undefined; - } -} + /** + * @openapi + * /plugin-api/host-metrics/metrics/connect-totp: + * post: + * summary: Submit a TOTP code + * description: Answers the TOTP prompt a metrics connection is waiting on. + * tags: + * - Host Metrics + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * sessionId: + * type: string + * totpCode: + * type: string + * responses: + * 200: + * description: Connected. + * 400: + * description: Missing sessionId or totpCode. + * 401: + * description: TOTP verification failed. + * 404: + * description: TOTP session not found or expired. + * 429: + * description: Too many TOTP attempts. + */ + router.post("/metrics/connect-totp", async (req, res) => { + const userId = actor(res); + if (!userId) return; + const { sessionId, totpCode } = (req.body ?? {}) as { + sessionId?: unknown; + totpCode?: unknown; + }; + if (typeof sessionId !== "string" || typeof totpCode !== "string") { + return res.status(400).json({ error: "Missing sessionId or totpCode" }); + } + const result = await submitTotp(interactive, userId, sessionId, totpCode); + res.status(result.status).json(result.body); + }); -function getPoolKey(host: SSHHostWithCredentials): string { - return pluginSsh().poolKey("stats", host as unknown as PluginSshHost); -} - -async function withSshConnection( - host: SSHHostWithCredentials, - fn: (client: Client) => Promise, -): Promise { - // Background polling has nobody to answer a TOTP prompt, so the pooled - // connection answers password prompts only and fails fast otherwise. - return withPooledSsh( - host as unknown as PluginSshHost, - { pool: "stats", purpose: "metrics", overrides: { readyTimeout: 60000 } }, - fn, - ); -} - -async function collectMetrics( - host: SSHHostWithCredentials, - onAuthenticated?: () => void, -): Promise<{ - cpu: { - percent: number | null; - cores: number | null; - load: [number, number, number] | null; - }; - memory: { - percent: number | null; - usedGiB: number | null; - totalGiB: number | null; - }; - disk: { - percent: number | null; - usedHuman: string | null; - totalHuman: string | null; - availableHuman: string | null; - mount: string | null; - filesystems: DiskFilesystem[]; - }; - network: { - interfaces: Array<{ - name: string; - ip: string; - state: string; - rxBytes: string | null; - txBytes: string | null; - rxRateBps: number | null; - txRateBps: number | null; - }>; - }; - uptime: { - seconds: number | null; - formatted: string | null; - }; - processes: { - total: number | null; - running: number | null; - top: Array<{ - pid: string; - user: string; - cpu: string; - mem: string; - command: string; - }>; - }; - system: { - hostname: string | null; - kernel: string | null; - os: string | null; - }; -}> { - if (!supportsMetrics(host)) { - throw new Error("Metrics collection only supported for SSH hosts"); - } - - if (authFailureTracker.shouldSkip(host.id)) { - const reason = authFailureTracker.getSkipReason(host.id); - throw new Error(reason || "Authentication failed"); - } - - const cached = metricsCache.get(host.id); - if (cached) { - onAuthenticated?.(); - return cached as ReturnType extends Promise - ? T - : never; - } - - return requestQueue.queueRequest(host.id, async () => { - const sessionKey = getSessionKey(host.id, host.userId!); - const existingSession = metricsSessions[sessionKey]; + /** + * @openapi + * /plugin-api/host-metrics/metrics/heartbeat: + * post: + * summary: Keep a metrics viewer alive + * description: A viewer without a heartbeat for two minutes is dropped. + * tags: + * - Host Metrics + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * viewerSessionId: + * type: string + * responses: + * 200: + * description: Heartbeat updated. + * 400: + * description: Invalid viewerSessionId. + * 404: + * description: Viewer session not found. + */ + router.post("/metrics/heartbeat", (req, res) => { + const userId = actor(res); + if (!userId) return; + const { viewerSessionId } = (req.body ?? {}) as { + viewerSessionId?: unknown; + }; + if (typeof viewerSessionId !== "string" || !viewerSessionId) { + return res.status(400).json({ error: "Invalid viewerSessionId" }); + } + if (poller.updateHeartbeat(viewerSessionId, userId)) { + return res.json({ success: true }); + } + res.status(404).json({ error: "Viewer session not found" }); + }); + /** + * @openapi + * /plugin-api/host-metrics/metrics/register-viewer: + * post: + * summary: Register a metrics viewer + * description: Starts polling a host while something shows its metrics. Answers skipped instead of failing when the host has no metrics. + * tags: + * - Host Metrics + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * hostId: + * type: integer + * responses: + * 200: + * description: Viewer registered, or skipped with a reason. + * 400: + * description: Invalid hostId. + */ + router.post("/metrics/register-viewer", async (req, res) => { + const userId = actor(res); + if (!userId) return; + const { hostId } = (req.body ?? {}) as { hostId?: unknown }; + if ( + typeof hostId !== "number" || + !Number.isInteger(hostId) || + hostId <= 0 + ) { + return res.status(400).json({ error: "Invalid hostId" }); + } + const skipped = (reason: string) => + res.json({ success: true, skipped: true, reason }); try { - const excludedMounts = pollingManager.parseStatsConfig( - host.statsConfig, - ).excludedMounts; - const monitoredMounts = pollingManager.parseStatsConfig( - host.statsConfig, - ).monitoredMounts; + const host = await poller.resolve(hostId, userId); + if (!host) return skipped("host_not_found"); + if (!supportsMetrics(host, ctx.ssh)) + return skipped("metrics_unsupported"); + if (!(await poller.settingsFor(hostId)).metricsEnabled) { + return skipped("metrics_disabled"); + } + const viewerSessionId = newSessionId("viewer"); + poller.registerViewer(hostId, viewerSessionId, userId); + res.json({ success: true, viewerSessionId }); + } catch (error) { + log.warn("Failed to register viewer", { + operation: "register_viewer_error", + hostId, + error: error instanceof Error ? error.message : String(error), + }); + res.json({ success: false, skipped: true, reason: "internal_error" }); + } + }); - const collectFn = async (client: Client) => { - onAuthenticated?.(); - const platform = await detectPlatform(client); - const cpu = await collectCpuMetrics(client, platform, host.id); - const memory = await collectMemoryMetrics(client, platform); - const disk = await collectDiskMetrics( - client, - excludedMounts, - monitoredMounts, - platform, - ); - const network = await collectNetworkMetrics(client, platform, host.id); - const uptime = await collectUptimeMetrics(client, platform); - const processes = await collectProcessesMetrics(client); - const system = await collectSystemMetrics(client, platform); + /** + * @openapi + * /plugin-api/host-metrics/metrics/unregister-viewer: + * post: + * summary: Unregister a metrics viewer + * description: Drops a viewer. Polling stops with the last one. + * tags: + * - Host Metrics + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * hostId: + * type: integer + * viewerSessionId: + * type: string + * responses: + * 200: + * description: Viewer unregistered. + * 400: + * description: Invalid hostId or viewerSessionId. + */ + router.post("/metrics/unregister-viewer", (req, res) => { + const userId = actor(res); + if (!userId) return; + const { hostId, viewerSessionId } = (req.body ?? {}) as { + hostId?: unknown; + viewerSessionId?: unknown; + }; + if (typeof hostId !== "number" || !hostId) { + return res.status(400).json({ error: "Invalid hostId" }); + } + if (typeof viewerSessionId !== "string" || !viewerSessionId) { + return res.status(400).json({ error: "Invalid viewerSessionId" }); + } + poller.unregisterViewer(hostId, viewerSessionId, userId); + res.json({ success: true }); + }); - let login_stats = { - recentLogins: [], - failedLogins: [], - totalLogins: 0, - uniqueIPs: 0, - }; - try { - login_stats = await collectLoginStats(client); - } catch { - // expected + /** + * @openapi + * /plugin-api/host-metrics/metrics/history/{id}: + * get: + * summary: Get metrics history + * description: Returns stored CPU, memory, disk and network samples for a host over a range. + * tags: + * - Host Metrics + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * - in: query + * name: range + * schema: + * type: string + * enum: [1h, 6h, 24h, 7d, 30d] + * - in: query + * name: from + * schema: + * type: string + * - in: query + * name: to + * schema: + * type: string + * responses: + * 200: + * description: The samples in the range. + * 400: + * description: Invalid range or dates. + * 403: + * description: No access to this host. + */ + router.get("/metrics/history/:id", validateHostId, async (req, res) => { + const hostId = Number(req.params.id); + if (!actor(res)) return; + try { + if (!(await canConnect(hostId))) { + return res.status(403).json({ error: "Access denied" }); + } + const { range, from, to } = req.query as Record< + string, + string | undefined + >; + let fromDate: Date; + let toDate = new Date(); + if (range) { + const offsetMs = RANGE_OFFSETS[range]; + if (!offsetMs) { + return res + .status(400) + .json({ error: "Invalid range. Use 1h, 6h, 24h, 7d, or 30d" }); } - - let ports: { - source: "ss" | "netstat" | "none"; - ports: Array<{ - protocol: "tcp" | "udp"; - localAddress: string; - localPort: number; - state?: string; - pid?: number; - process?: string; - }>; - } = { - source: "none", - ports: [], - }; - try { - ports = await collectPortsMetrics(client); - } catch { - // expected - } - - let firewall: { - type: "iptables" | "nftables" | "none"; - status: "active" | "inactive" | "unknown"; - chains: Array<{ - name: string; - policy: string; - rules: Array<{ - chain: string; - target: string; - protocol: string; - source: string; - destination: string; - dport?: string; - sport?: string; - state?: string; - interface?: string; - extra?: string; - }>; - }>; - } = { - type: "none", - status: "unknown", - chains: [], - }; - try { - firewall = await collectFirewallMetrics(client); - } catch { - // expected - } - - let temperature: { - source: "sysfs" | "sensors" | "none"; - highestCelsius: number | null; - sensors: Array<{ label: string; celsius: number }>; - } = { - source: "none", - highestCelsius: null, - sensors: [], - }; - try { - temperature = await collectTemperatureMetrics(client); - } catch { - // expected - } - - let gpu: GpuMetrics = { source: "none", gpus: [], processes: [] }; - try { - gpu = await collectGpuMetrics(client); - } catch { - // expected - } - - const result = { - cpu, - memory, - disk, - network, - uptime, - processes, - system, - login_stats, - ports, - firewall, - temperature, - gpu, - }; - - metricsCache.set(host.id, result); - return result; - }; - - if (existingSession && existingSession.isConnected) { - existingSession.activeOperations++; - try { - const result = await collectFn(existingSession.client); - existingSession.lastActive = Date.now(); - return result; - } finally { - existingSession.activeOperations--; + fromDate = new Date(Date.now() - offsetMs); + } else if (from && to) { + fromDate = new Date(from); + toDate = new Date(to); + if (isNaN(fromDate.getTime()) || isNaN(toDate.getTime())) { + return res.status(400).json({ error: "Invalid from/to date format" }); } } else { - return await withSshConnection(host, collectFn); + fromDate = new Date(Date.now() - 24 * 60 * 60 * 1000); } - } catch (error) { - if (error instanceof Error) { - if (error.message.includes("TOTP authentication required")) { - throw error; - } else if ( - error.message.includes("No password available") || - error.message.includes("Unsupported authentication type") || - error.message.includes("No SSH key available") || - error.message.includes("Invalid SSH key format") - ) { - authFailureTracker.recordFailure(host.id, "AUTH", true); - } else if (isHostKeyVerificationError(error)) { - authFailureTracker.recordFailure(host.id, "HOST_KEY", true); - } else if ( - error.message.includes("authentication") || - error.message.includes("Permission denied") || - error.message.includes("All configured authentication methods failed") - ) { - authFailureTracker.recordFailure(host.id, "AUTH"); - } else if ( - error.message.includes("timeout") || - error.message.includes("ETIMEDOUT") - ) { - authFailureTracker.recordFailure(host.id, "TIMEOUT"); - } - } - throw error; - } - }); -} -function tcpPing( - host: string, - port: number, - timeoutMs = 5000, -): Promise { - return new Promise((resolve) => { - const socket = new net.Socket(); - let settled = false; - - const finish = (result: boolean) => { - if (settled) return; - settled = true; - resolve(result); - }; - - const cleanup = () => { - try { - socket.destroy(); - } catch { - // expected - } - }; - - socket.setTimeout(timeoutMs); - - socket.once("connect", () => { - const dataTimeout = setTimeout(() => { - cleanup(); - finish(true); - }, 2000); - - socket.once("data", (data) => { - clearTimeout(dataTimeout); - const dataStr = data.toString("utf8"); - if (dataStr.startsWith("SSH-")) { - try { - socket.end("SSH-2.0-TermixHealthCheck\r\n"); - } catch { - // expected - } - setTimeout(cleanup, 200); - } else { - cleanup(); - } - finish(true); - }); - }); - - socket.once("timeout", () => { - cleanup(); - finish(false); - }); - socket.once("error", () => { - cleanup(); - finish(false); - }); - socket.connect(port, host); - }); -} - -/** - * @openapi - * /status: - * get: - * summary: Get all host statuses - * description: Retrieves the status of all hosts for the authenticated user. - * tags: - * - Host Metrics - * responses: - * 200: - * description: A map of host IDs to their status entries. - * 401: - * description: Session expired - please log in again. - */ -app.get("/status", async (req, res) => { - const userId = (req as AuthenticatedRequest).userId; - - if (DataCrypto.getUserDataKey(userId) === null) { - return res.status(401).json({ - error: "Session expired - please log in again", - code: "SESSION_EXPIRED", - }); - } - - const requestedHostIds = parseStatusHostIds(req.query.hostIds); - if (requestedHostIds !== null) { - await pollingManager.reconcileStatusPolling(userId, requestedHostIds); - } else if (pollingManager.getAllStatuses().size === 0) { - await pollingManager.initializePolling(userId); - } - - // One batched permission resolution for the whole fleet; this endpoint is - // polled every few seconds, and a per-host check made it linear in host - // count against the database. - const entries = Array.from(pollingManager.getAllStatuses().entries()); - const allowed = await permissionManager.filterAccessibleHostIds( - userId, - entries.map(([id]) => id), - ); - - const result: Record = {}; - for (const [id, entry] of entries) { - if ( - allowed.has(id) && - (requestedHostIds === null || requestedHostIds.has(id)) - ) { - result[id] = entry; - } - } - res.json(result); -}); - -/** - * @openapi - * /status/{id}: - * get: - * summary: Get host status by ID - * description: Retrieves the status of a specific host by its ID. - * tags: - * - Host Metrics - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: integer - * responses: - * 200: - * description: Host status entry. - * 401: - * description: Session expired - please log in again. - * 404: - * description: Status not available. - */ -app.get("/status/:id", validateHostId, async (req, res) => { - const id = Number(req.params.id); - const userId = (req as AuthenticatedRequest).userId; - - if (DataCrypto.getUserDataKey(userId) === null) { - return res.status(401).json({ - error: "Session expired - please log in again", - code: "SESSION_EXPIRED", - }); - } - - const access = await permissionManager.canAccessHost(userId, id, "connect"); - if (!access.hasAccess) { - return res.status(404).json({ error: "Status not available" }); - } - - const statuses = pollingManager.getAllStatuses(); - if (statuses.size === 0) { - await pollingManager.initializePolling(userId); - } - - const statusEntry = pollingManager.getStatus(id); - if (!statusEntry) { - return res.status(404).json({ error: "Status not available" }); - } - - res.json(statusEntry); -}); - -/** - * @openapi - * /clear-connections: - * post: - * summary: Clear all SSH connections - * description: Clears all SSH connections from the connection pool. - * tags: - * - Host Metrics - * responses: - * 200: - * description: All SSH connections cleared. - * 401: - * description: Session expired - please log in again. - */ -app.post("/clear-connections", requireAdmin, async (req, res) => { - const userId = (req as AuthenticatedRequest).userId; - - if (DataCrypto.getUserDataKey(userId) === null) { - return res.status(401).json({ - error: "Session expired - please log in again", - code: "SESSION_EXPIRED", - }); - } - - connectionPool.clearAllConnections(); - res.json({ message: "All SSH connections cleared" }); -}); - -/** - * @openapi - * /refresh: - * post: - * summary: Refresh polling - * description: Refreshes host polling for the authenticated user. - * tags: - * - Host Metrics - * responses: - * 200: - * description: Polling refreshed. - * 401: - * description: Session expired - please log in again. - */ -app.post("/refresh", async (req, res) => { - const userId = (req as AuthenticatedRequest).userId; - - if (DataCrypto.getUserDataKey(userId) === null) { - return res.status(401).json({ - error: "Session expired - please log in again", - code: "SESSION_EXPIRED", - }); - } - - await pollingManager.refreshHostPolling(userId); - res.json({ message: "Polling refreshed" }); -}); - -/** - * Reacts to a host being created or updated somewhere in core. - * - * Used to be POST /host-updated on this plugin's own port. An event now, so - * there is no service-to-service HTTP call and no port. The acting user comes - * with the event because re-reading a host needs their data key. - */ -export async function onHostUpdated(payload: { - hostId: number; - userId: string; -}): Promise { - const { hostId, userId } = payload; - if (!hostId || !userId) return; - - // No data key means that user is not unlocked on this process, so there is - // nothing to decrypt the host with. Polling resumes when they next log in. - if (DataCrypto.getUserDataKey(userId) === null) return; - - try { - hostPollCache.invalidate(hostId); - const host = await fetchHostById(hostId, userId); - if (!host) return; - - connectionPool.clearKeyConnections(getPoolKey(host)); - await pollingManager.startPollingForHost(host); - } catch (error) { - statsLogger.error("Failed to start polling for host", error, { - operation: "host_updated", - hostId, - userId, - }); - } -} - -/** Reacts to a host being deleted. Was POST /host-deleted. */ -export function onHostDeleted(payload: { hostId: number }): void { - const { hostId } = payload; - if (!hostId) return; - - try { - pollingManager.stopPollingForHost(hostId, true); - } catch (error) { - statsLogger.error("Failed to stop polling for host", error, { - operation: "host_deleted", - hostId, - }); - } -} - -/** Reacts to an SSH login. Was the internal POST /internal/login-alert. */ -export function onHostLogin(payload: { - hostId: number; - userId: string; - sshUser: string; - fromIp: string; -}): void { - const { hostId, userId, sshUser, fromIp } = payload; - if (!hostId || !userId) return; - notifyAutomationInternalEvent("user_login", userId, hostId, { - sshUser, - fromIp, - }); -} - -/** - * @openapi - * /metrics/{id}: - * get: - * summary: Get host metrics - * description: Retrieves current metrics for a specific host including CPU, memory, disk, network, processes, and system information. - * tags: - * - Host Metrics - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: integer - * responses: - * 200: - * description: Host metrics data. - * 401: - * description: Session expired - please log in again. - * 404: - * description: Metrics not available. - */ -app.get("/metrics/:id", validateHostId, async (req, res) => { - const id = Number(req.params.id); - const userId = (req as AuthenticatedRequest).userId; - - if (DataCrypto.getUserDataKey(userId) === null) { - return res.status(401).json({ - error: "Session expired - please log in again", - code: "SESSION_EXPIRED", - }); - } - - const metricsData = pollingManager.getMetrics(id); - if (!metricsData) { - return res.status(404).json({ - error: "Metrics not available", - cpu: { percent: null, cores: null, load: null }, - memory: { percent: null, usedGiB: null, totalGiB: null }, - disk: { - percent: null, - usedHuman: null, - totalHuman: null, - availableHuman: null, - mount: null, - filesystems: [], - }, - network: { interfaces: [] }, - uptime: { seconds: null, formatted: null }, - processes: { total: null, running: null, top: [] }, - system: { hostname: null, kernel: null, os: null }, - lastChecked: new Date().toISOString(), - }); - } - - res.json({ - ...metricsData.data, - lastChecked: new Date(metricsData.timestamp).toISOString(), - }); -}); - -/** - * @openapi - * /metrics/start/{id}: - * post: - * summary: Start metrics collection - * description: Establishes an SSH connection and starts collecting metrics for a specific host. - * tags: - * - Host Metrics - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: integer - * responses: - * 200: - * description: Metrics collection started successfully, or TOTP required. - * 401: - * description: Session expired - please log in again. - * 404: - * description: Host not found. - * 500: - * description: Failed to start metrics collection. - */ -app.post("/metrics/start/:id", validateHostId, async (req, res) => { - const id = Number(req.params.id); - const userId = (req as AuthenticatedRequest).userId; - - const connectionLogs: Array> = []; - - if (DataCrypto.getUserDataKey(userId) === null) { - connectionLogs.push( - createConnectionLog("error", "stats_connecting", "Session expired"), - ); - return res.status(401).json({ - error: "Session expired - please log in again", - code: "SESSION_EXPIRED", - connectionLogs, - }); - } - - try { - const host = await fetchHostById(id, userId); - if (!host) { - connectionLogs.push( - createConnectionLog("error", "stats_connecting", "Host not found"), - ); - return res.status(404).json({ error: "Host not found", connectionLogs }); - } - - connectionLogs.push( - createConnectionLog( - "info", - "stats_connecting", - "Starting metrics collection", - ), - ); - - connectionLogs.push( - createConnectionLog("info", "dns", `Resolving DNS for ${host.ip}`), - ); - - connectionLogs.push( - createConnectionLog( - "info", - "tcp", - `Connecting to ${host.ip}:${host.port}`, - ), - ); - - connectionLogs.push( - createConnectionLog("info", "handshake", "Initiating SSH handshake"), - ); - - if (host.authType === "password") { - connectionLogs.push( - createConnectionLog("info", "auth", "Authenticating with password"), - ); - } else if (host.authType === "key") { - connectionLogs.push( - createConnectionLog("info", "auth", "Authenticating with SSH key"), - ); - } - - const sessionKey = getSessionKey(host.id, userId); - - const existingSession = metricsSessions[sessionKey]; - if (existingSession && existingSession.isConnected) { - connectionLogs.push( - createConnectionLog( - "success", - "stats_polling", - "Using existing metrics session", - ), - ); - return res.json({ success: true, connectionLogs }); - } - - const client = new Client(); - const ssh = pluginSsh(); - const prepared = await ssh.prepare(host as unknown as PluginSshHost, { - purpose: "metrics", - client, - log: (level, message) => - connectionLogs.push(createConnectionLog(level, "auth", message)), - }); - const config = prepared.config as ConnectConfig; - config.readyTimeout = 60000; - if (prepared.outcome.status !== "ready") { - const outcome = prepared.outcome; - connectionLogs.push( - createConnectionLog("error", "auth", outcome.message), - ); - if (outcome.status === "interaction-required") { - return res.status(401).json({ - error: outcome.message, - requiresAuthInteraction: outcome.interaction, - ...(outcome.flag ? { [outcome.flag]: true } : {}), - connectionLogs, - }); - } - return res.status(400).json({ error: outcome.message, connectionLogs }); - } - - const connectionPromise = new Promise<{ - success: boolean; - requires_totp?: boolean; - sessionId?: string; - prompt?: string; - viewerSessionId?: string; - }>((resolve, reject) => { - let isResolved = false; - - const timeout = setTimeout(() => { - if (!isResolved) { - isResolved = true; - client.end(); - reject(new Error("Connection timeout")); - } - }, 60000); - - client.on( - "keyboard-interactive", - (name, instructions, _instructionsLang, prompts, finish) => { - const decision = ssh.classifyKeyboardInteractive( - { name, instructions, prompts }, - host as unknown as PluginSshHost, - ); - const totpPromptIndex = - decision.kind === "totp" ? decision.promptIndex : -1; - - if (totpPromptIndex !== -1) { - const sessionId = `totp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; - - pendingTOTPSessions[sessionId] = { - client, - finish, - config, - createdAt: Date.now(), - sessionId, - hostId: host.id, - userId: host.userId!, - prompts: prompts.map((p) => ({ - prompt: p.prompt, - echo: p.echo ?? false, - })), - totpPromptIndex, - resolvedPassword: host.password, - totpAttempts: 0, - }; - - connectionLogs.push( - createConnectionLog( - "info", - "stats_totp", - "TOTP verification required", - ), - ); - - clearTimeout(timeout); - if (!isResolved) { - isResolved = true; - resolve({ - success: false, - requires_totp: true, - sessionId, - prompt: prompts[totpPromptIndex].prompt, - }); - } - return; - } else { - finish(ssh.autoResponses(prompts, host.password)); - } - }, - ); - - client.on("ready", () => { - clearTimeout(timeout); - if (!isResolved) { - isResolved = true; - - connectionLogs.push( - createConnectionLog( - "success", - "connected", - "SSH connection established successfully", - ), - ); - - connectionLogs.push( - createConnectionLog( - "success", - "stats_polling", - "Metrics session established", - ), - ); - - metricsSessions[sessionKey] = { - client, - isConnected: true, - lastActive: Date.now(), - activeOperations: 0, - hostId: host.id, - userId, - }; - scheduleMetricsSessionCleanup(sessionKey); - - const viewerSessionId = `viewer-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; - pollingManager.registerViewer(host.id, viewerSessionId, userId); - - resolve({ success: true, viewerSessionId }); - } - }); - - client.on("error", (error) => { - clearTimeout(timeout); - if (!isResolved) { - isResolved = true; - - const errorMessage = - error instanceof Error ? error.message : String(error); - let errorStage: ConnectionStage; - - if ( - errorMessage.includes("ENOTFOUND") || - errorMessage.includes("getaddrinfo") - ) { - errorStage = "dns"; - connectionLogs.push( - createConnectionLog( - "error", - errorStage, - `DNS resolution failed: ${errorMessage}`, - ), - ); - } else if ( - errorMessage.includes("ECONNREFUSED") || - errorMessage.includes("ETIMEDOUT") - ) { - errorStage = "tcp"; - connectionLogs.push( - createConnectionLog( - "error", - errorStage, - `TCP connection failed: ${errorMessage}`, - ), - ); - } else if ( - errorMessage.includes("handshake") || - errorMessage.includes("key exchange") - ) { - errorStage = "handshake"; - connectionLogs.push( - createConnectionLog( - "error", - errorStage, - `SSH handshake failed: ${errorMessage}`, - ), - ); - } else if ( - errorMessage.includes("authentication") || - errorMessage.includes("Authentication") - ) { - errorStage = "auth"; - connectionLogs.push( - createConnectionLog( - "error", - errorStage, - `Authentication failed: ${errorMessage}`, - ), - ); - } else if (errorMessage.includes("verification failed")) { - errorStage = "handshake"; - connectionLogs.push( - createConnectionLog( - "error", - errorStage, - `SSH host key has changed. For security, please open a Terminal connection to this host first to verify and accept the new key fingerprint.`, - ), - ); - } else { - connectionLogs.push( - createConnectionLog( - "error", - "error", - `SSH connection failed: ${errorMessage}`, - ), - ); - } - - statsLogger.error("SSH connection error in metrics/start", { - operation: "metrics_start_ssh_error", - hostId: host.id, - error: errorMessage, - }); - reject(error); - } - }); - - if (host.jumpHosts && host.jumpHosts.length > 0) { - connectionLogs.push( - createConnectionLog( - "info", - "proxy", - "Connecting via jump host chain", - ), - ); - } - ssh - .openTransport( - host as unknown as PluginSshHost, - config as Record, + const rows = ( + await repository.listSamples( + hostId, + sqlTimestamp(fromDate), + sqlTimestamp(toDate), ) - .then((transport) => { - if (transport.jumpClient) { - const jumpClient = transport.jumpClient as Client; - client.on("close", () => jumpClient.end()); - } - client.connect(config); - }) - .catch((error) => { - if (!isResolved) { - isResolved = true; - clearTimeout(timeout); - connectionLogs.push( - createConnectionLog( - "error", - "proxy", - `Connection setup failed: ${getErrorMessage(error)}`, - ), - ); - reject(error); - } - }); - }); - - const result = await connectionPromise; - res.json({ ...result, connectionLogs }); - } catch (error) { - statsLogger.error("Failed to start metrics collection", { - operation: "metrics_start_error", - hostId: id, - error: error instanceof Error ? error.message : String(error), - }); - connectionLogs.push( - createConnectionLog( - "error", - "stats_connecting", - `Failed to start metrics: ${getErrorMessage(error)}`, - ), - ); - res.status(500).json({ - error: getErrorMessage(error, "Failed to start metrics collection"), - connectionLogs, - }); - } -}); - -/** - * @openapi - * /metrics/stop/{id}: - * post: - * summary: Stop metrics collection - * description: Stops metrics collection for a specific host and cleans up the SSH session. - * tags: - * - Host Metrics - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: integer - * requestBody: - * required: false - * content: - * application/json: - * schema: - * type: object - * properties: - * viewerSessionId: - * type: string - * responses: - * 200: - * description: Metrics collection stopped successfully. - * 401: - * description: Session expired - please log in again. - * 500: - * description: Failed to stop metrics collection. - */ -app.post("/metrics/stop/:id", validateHostId, async (req, res) => { - const id = Number(req.params.id); - const userId = (req as AuthenticatedRequest).userId; - const { viewerSessionId } = req.body; - - if (DataCrypto.getUserDataKey(userId) === null) { - return res.status(401).json({ - error: "Session expired - please log in again", - code: "SESSION_EXPIRED", - }); - } - - try { - const sessionKey = getSessionKey(id, userId); - const session = metricsSessions[sessionKey]; - - if (session) { - cleanupMetricsSession(sessionKey); - } - - if (viewerSessionId && typeof viewerSessionId === "string") { - pollingManager.unregisterViewer(id, viewerSessionId); - } else { - pollingManager.stopMetricsOnly(id); - } - - res.json({ success: true }); - } catch (error) { - statsLogger.error("Failed to stop metrics collection", { - operation: "metrics_stop_error", - hostId: id, - error: error instanceof Error ? error.message : String(error), - }); - res.status(500).json({ - error: getErrorMessage(error, "Failed to stop metrics collection"), - }); - } -}); - -/** - * @openapi - * /metrics/connect-totp: - * post: - * summary: Complete TOTP verification for metrics - * description: Verifies the TOTP code and completes the metrics SSH connection. - * tags: - * - Host Metrics - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * sessionId: - * type: string - * totpCode: - * type: string - * responses: - * 200: - * description: TOTP verified, metrics connection established. - * 400: - * description: Missing sessionId or totpCode. - * 401: - * description: Session expired or invalid TOTP code. - * 404: - * description: TOTP session not found or expired. - * 500: - * description: Failed to verify TOTP. - */ -app.post("/metrics/connect-totp", async (req, res) => { - const { sessionId, totpCode } = req.body; - const userId = (req as AuthenticatedRequest).userId; - - if (DataCrypto.getUserDataKey(userId) === null) { - return res.status(401).json({ - error: "Session expired - please log in again", - code: "SESSION_EXPIRED", - }); - } - - if (!sessionId || !totpCode) { - return res.status(400).json({ error: "Missing sessionId or totpCode" }); - } - - const session = pendingTOTPSessions[sessionId]; - if (!session) { - return res.status(404).json({ error: "TOTP session not found or expired" }); - } - - if (Date.now() - session.createdAt > 180000) { - delete pendingTOTPSessions[sessionId]; - try { - session.client.end(); - } catch { - // expected - } - return res.status(408).json({ error: "TOTP session timeout" }); - } - - if (session.userId !== userId) { - return res.status(403).json({ error: "Unauthorized" }); - } - - session.totpAttempts++; - if (session.totpAttempts > 3) { - delete pendingTOTPSessions[sessionId]; - try { - session.client.end(); - } catch { - // expected - } - return res.status(429).json({ error: "Too many TOTP attempts" }); - } - - try { - const responses = (session.prompts || []).map((p, idx) => { - if (idx === session.totpPromptIndex) { - return totpCode.trim(); - } else if (/password/i.test(p.prompt) && session.resolvedPassword) { - return session.resolvedPassword; - } - return ""; - }); - - const connectionPromise = new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - reject(new Error("TOTP verification timeout")); - }, 30000); - - session.client.once( - "keyboard-interactive", - (name, instructions, instructionsLang, prompts, finish) => { - statsLogger.warn("Second keyboard-interactive received after TOTP", { - operation: "totp_second_keyboard_interactive", - hostId: session.hostId, - sessionId, - prompts: prompts.map((p) => p.prompt), - }); - const secondResponses = prompts.map((p) => { - if (/password/i.test(p.prompt) && session.resolvedPassword) { - return session.resolvedPassword; - } - return ""; - }); - finish(secondResponses); - }, - ); - - session.client.once("ready", () => { - clearTimeout(timeout); - resolve(); + ).map((row) => ({ + ts: row.ts, + cpu_percent: row.cpuPercent, + mem_percent: row.memPercent, + disk_percent: row.diskPercent, + net_rx_bytes: row.netRxBytes, + net_tx_bytes: row.netTxBytes, + })); + res.json({ + rows, + fromTs: fromDate.toISOString(), + toTs: toDate.toISOString(), }); - - session.client.once("error", (error) => { - clearTimeout(timeout); - statsLogger.error("SSH client error after TOTP", { - operation: "totp_client_error", - hostId: session.hostId, - sessionId, - error: error instanceof Error ? error.message : String(error), - }); - reject(error); + } catch (error) { + log.error("Failed to fetch metrics history", error, { + operation: "metrics_history_fetch_error", + hostId, }); - }); + res.status(500).json({ error: "Failed to fetch metrics history" }); + } + }); - session.finish(responses); - - await connectionPromise; - - const sessionKey = getSessionKey(session.hostId, userId); - metricsSessions[sessionKey] = { - client: session.client, - isConnected: true, - lastActive: Date.now(), - activeOperations: 0, - hostId: session.hostId, - userId, - }; - scheduleMetricsSessionCleanup(sessionKey); - - delete pendingTOTPSessions[sessionId]; - - const viewerSessionId = `viewer-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; - pollingManager.registerViewer(session.hostId, viewerSessionId, userId); - - res.json({ success: true, viewerSessionId }); - } catch (error) { - statsLogger.error("TOTP verification failed", { - operation: "totp_verification_failed", - hostId: session.hostId, - sessionId, - error: error instanceof Error ? error.message : String(error), - }); - - if (session.totpAttempts >= 3) { - delete pendingTOTPSessions[sessionId]; + /** + * @openapi + * /plugin-api/host-metrics/host-metrics/preferences/{id}: + * get: + * summary: Get the Host Metrics layout for a host + * description: Returns the current user's saved card layout for the host, or a default layout from the host's enabled widgets when none is saved. + * tags: + * - Host Metrics + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: The layout for this host. + * 403: + * description: No access to this host. + */ + router.get( + "/host-metrics/preferences/:id", + validateHostId, + async (req, res) => { + const userId = actor(res); + if (!userId) return; + const hostId = Number(req.params.id); try { - session.client.end(); - } catch { - // expected + if (!(await canConnect(hostId))) { + return res.status(403).json({ error: "No access to this host" }); + } + const saved = await repository.findLayout(userId, hostId); + if (saved) { + try { + const parsed = sanitizeLayout(JSON.parse(saved)); + if (parsed) return res.json({ layout: parsed }); + } catch { + // fall through to the default + } + } + const settings = await poller.settingsFor(hostId); + res.json({ layout: defaultLayoutFromWidgets(settings.enabledWidgets) }); + } catch (error) { + log.error("Failed to fetch host metrics preferences", error, { + operation: "host_metrics_prefs_fetch_error", + hostId, + }); + res + .status(500) + .json({ error: "Failed to fetch host metrics preferences" }); } - } - - res.status(401).json({ - error: "TOTP verification failed", - attemptsRemaining: Math.max(0, 3 - session.totpAttempts), - }); - } -}); - -// Lets automations keep metrics flowing for hosts they watch, through the same -// viewer refcount the UI uses rather than around it. -import("../../../automations/src/backend/headless-viewer.js") - .then(({ setViewerRegistry }) => { - setViewerRegistry({ - registerViewer: (hostId, sessionId, userId) => - pollingManager.registerViewer(hostId, sessionId, userId), - unregisterViewer: (hostId, sessionId) => - pollingManager.unregisterViewer(hostId, sessionId), - updateHeartbeat: (sessionId) => pollingManager.updateHeartbeat(sessionId), - }); - }) - .catch(() => {}); - -registerHostMetricsViewerRoutes(app, { - fetchHostById, - supportsMetrics: (host: SSHHostWithCredentials) => supportsMetrics(host), - parseStatsConfig: (statsConfig: SSHHostWithCredentials["statsConfig"]) => - pollingManager.parseStatsConfig(statsConfig), - updateHeartbeat: (viewerSessionId) => - pollingManager.updateHeartbeat(viewerSessionId), - registerViewer: (hostId, viewerSessionId, userId) => - pollingManager.registerViewer(hostId, viewerSessionId, userId), - unregisterViewer: (hostId, viewerSessionId) => - pollingManager.unregisterViewer(hostId, viewerSessionId), -}); - -registerHostMetricsSettingsRoutes(app, { - requireAdmin, - defaultStatsConfig: DEFAULT_STATS_CONFIG, - refreshAllPolling: () => pollingManager.refreshAllPolling(), -}); - -registerHostMetricsPreferencesRoutes(app, { - validateHostId, - fetchHostById: async (hostId, userId) => - (await fetchHostById(hostId, userId)) ?? null, - parseStatsConfig: (statsConfig) => - pollingManager.parseStatsConfig( - statsConfig as string | StatsConfig | undefined, - ), - canAccessHost: async (userId, hostId, level) => - (await permissionManager.canAccessHost(userId, hostId, level)).hasAccess, -}); - -registerHostMetricsHistoryRoutes(app, { - validateHostId, - canAccessHost: async (userId, hostId, level) => - (await permissionManager.canAccessHost(userId, hostId, level)).hasAccess, -}); - -registerManagerRoutes(app, { - validateHostId, - runOnHost: async (hostId, userId, level, fn) => { - const access = await permissionManager.canAccessHost(userId, hostId, level); - if (!access.hasAccess) { - throw new AccessDeniedError(); - } - const host = await fetchHostById(hostId, userId); - if (!host) { - throw new AccessDeniedError("Host not found"); - } - // sudoPassword is encrypted with the owner's key; when the viewer is a - // different user, re-fetch under the owner's key to decrypt it correctly. - let sudoPassword = host.sudoPassword; - if (host.userId && host.userId !== userId) { - const ownerHost = await fetchHostById(hostId, host.userId); - if (ownerHost) sudoPassword = ownerHost.sudoPassword; - } - const managerHost: ManagerHost = { - id: host.id, - userId: host.userId, - sudoPassword, - enableDocker: !!(host as { enableDocker?: boolean }).enableDocker, - }; - return withSshConnection(host, (client) => fn(client, managerHost)); - }, -}); - -let cleanupInterval: NodeJS.Timeout | undefined; - -/** - * Mounts the Host Metrics routes on the router core provides. Called from the - * plugin's activate(); there is no port to bind any more. - */ -export async function startHostMetricsService(mountOn: Router): Promise { - mountOn.use(app); - - try { - await authManager.initialize(); - } catch (err) { - statsLogger.error("Failed to initialize AuthManager", err, { - operation: "auth_init_error", - }); - } - - cleanupInterval = setInterval( - () => { - authFailureTracker.cleanup(); - pollingBackoff.cleanup(); }, - 10 * 60 * 1000, ); -} -/** - * Releases everything the service holds: polling timers, pooled SSH - * connections and the periodic auth-failure cleanup. Called from the - * plugin's deactivate(), replacing the old SIGINT/SIGTERM handlers -- the - * process itself no longer owns this service's shutdown, the plugin - * lifecycle does. - */ -export function shutdown(): void { - if (cleanupInterval) { - clearInterval(cleanupInterval); - cleanupInterval = undefined; - } - pollingManager.destroy(); - // The pool is core's and shared with tmux and the fleet tools; destroying it - // here broke them until a restart. ctx.ssh drops this plugin's pooled - // connections on deactivate instead. + /** + * @openapi + * /plugin-api/host-metrics/host-metrics/preferences/{id}: + * post: + * summary: Save the Host Metrics layout for a host + * description: Saves the current user's card layout for the host. For the host's owner it also updates the host's enabled widgets. + * tags: + * - Host Metrics + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * slots: + * type: array + * columns: + * type: integer + * responses: + * 200: + * description: Layout saved. + * 400: + * description: Invalid layout. + * 403: + * description: No access to this host. + */ + router.post( + "/host-metrics/preferences/:id", + validateHostId, + async (req, res) => { + const userId = actor(res); + if (!userId) return; + const hostId = Number(req.params.id); + try { + const access = await ctx.hosts.checkAccess(hostId, "connect"); + if (!access.hasAccess) { + return res.status(403).json({ error: "No access to this host" }); + } + const layout = sanitizeLayout(req.body); + if (!layout) return res.status(400).json({ error: "Invalid layout" }); + + await repository.saveLayout(userId, hostId, JSON.stringify(layout)); + if (access.isOwner) { + await ctx.settings.setHost( + hostId, + "enabledWidgets", + deriveEnabledWidgets(layout.slots), + ); + } + res.json({ success: true }); + } catch (error) { + log.error("Failed to save host metrics preferences", error, { + operation: "host_metrics_prefs_save_error", + hostId, + }); + res + .status(500) + .json({ error: "Failed to save host metrics preferences" }); + } + }, + ); + + registerManagerRoutes(router, { + validateHostId, + log, + repository, + onHealthCheck: deps.onHealthCheck, + runOnHost: async (hostId, level, fn) => { + const userId = ctx.currentActor(); + if (!userId) throw new AccessDeniedError("Authentication required"); + const access = await ctx.hosts.checkAccess(hostId, level); + if (!access.hasAccess) throw new AccessDeniedError(); + const host = await poller.resolve(hostId, userId); + if (!host) throw new AccessDeniedError("Host not found"); + return ctx.ssh.withConnection( + host, + { + pool: "stats", + purpose: "metrics", + overrides: { readyTimeout: 60000 }, + }, + (client) => + fn(client as never, { + id: host.id, + userId: host.userId, + actorId: userId, + sudoPassword: sudoPasswordOf(host), + enableDocker: !!host.enableDocker, + }), + ); + }, + }); } diff --git a/plugins/host-metrics/src/backend/sessions.ts b/plugins/host-metrics/src/backend/sessions.ts index 87a5f4b01..048416300 100644 --- a/plugins/host-metrics/src/backend/sessions.ts +++ b/plugins/host-metrics/src/backend/sessions.ts @@ -1,16 +1,19 @@ import type { Client, ConnectConfig } from "ssh2"; -import { statsLogger } from "../../../../src/backend/utils/logger.js"; +import type { PluginSchedule } from "@termix/plugin-sdk/backend"; +import type { MetricsLogger } from "./log.js"; +/** A connection a person opened from the tab, reused by polling. */ export interface MetricsSession { client: Client; isConnected: boolean; lastActive: number; - timeout?: NodeJS.Timeout; activeOperations: number; hostId: number; userId: string; + cancelCleanup?: () => void; } +/** A connection waiting for the TOTP code the person is typing. */ export interface PendingTOTPSession { client: Client; finish: (responses: string[]) => void; @@ -32,49 +35,106 @@ export interface MetricsViewer { lastHeartbeat: number; } -export const metricsSessions: Record = {}; -export const pendingTOTPSessions: Record = {}; +const IDLE_MS = 30 * 60 * 1000; -export function cleanupMetricsSession(sessionId: string): void { - const session = metricsSessions[sessionId]; - if (session) { - if (session.activeOperations > 0) { - statsLogger.warn( - `Deferring metrics session cleanup - ${session.activeOperations} active operations`, - { - operation: "cleanup_deferred", - sessionId, - activeOperations: session.activeOperations, - }, - ); - scheduleMetricsSessionCleanup(sessionId); - return; - } - - try { - session.client.end(); - } catch { - // expected - } - clearTimeout(session.timeout); - delete metricsSessions[sessionId]; - } -} - -export function scheduleMetricsSessionCleanup(sessionId: string): void { - const session = metricsSessions[sessionId]; - if (session) { - if (session.timeout) clearTimeout(session.timeout); - - session.timeout = setTimeout( - () => { - cleanupMetricsSession(sessionId); - }, - 30 * 60 * 1000, - ); - } -} - -export function getSessionKey(hostId: number, userId: string): string { +export function sessionKey(hostId: number, userId: string): string { return `${userId}:${hostId}`; } + +function endQuietly(client: Client): void { + try { + client.end(); + } catch { + // expected + } +} + +/** Interactive connections, owned by one activation of the plugin. */ +export class MetricsSessions { + private readonly sessions = new Map(); + private readonly pending = new Map(); + + constructor( + private readonly schedule: PluginSchedule, + private readonly log: MetricsLogger, + ) {} + + get(key: string): MetricsSession | undefined { + return this.sessions.get(key); + } + + /** Stores a ready connection and closes it after 30 idle minutes. */ + open(key: string, session: Omit): void { + const existing = this.sessions.get(key); + if (existing && existing.client !== session.client) this.close(key); + this.sessions.set(key, { ...session }); + this.scheduleCleanup(key); + session.client.once("close", () => { + const current = this.sessions.get(key); + if (current?.client === session.client) { + current.isConnected = false; + current.cancelCleanup?.(); + this.sessions.delete(key); + } + }); + } + + touch(key: string): void { + const session = this.sessions.get(key); + if (!session) return; + session.lastActive = Date.now(); + this.scheduleCleanup(key); + } + + /** Closes a session, or waits for the operations still using it. */ + close(key: string): void { + const session = this.sessions.get(key); + if (!session) return; + if (session.activeOperations > 0) { + this.log.warn( + `Deferring metrics session cleanup - ${session.activeOperations} active operations`, + { operation: "cleanup_deferred", sessionKey: key }, + ); + this.scheduleCleanup(key); + return; + } + session.cancelCleanup?.(); + this.sessions.delete(key); + endQuietly(session.client); + } + + addPending(session: PendingTOTPSession): void { + this.pending.set(session.sessionId, session); + } + + getPending(sessionId: string): PendingTOTPSession | undefined { + return this.pending.get(sessionId); + } + + dropPending(sessionId: string, endClient = false): void { + const session = this.pending.get(sessionId); + this.pending.delete(sessionId); + if (session && endClient) endQuietly(session.client); + } + + dispose(): void { + for (const key of [...this.sessions.keys()]) { + const session = this.sessions.get(key)!; + session.cancelCleanup?.(); + endQuietly(session.client); + } + this.sessions.clear(); + for (const session of this.pending.values()) endQuietly(session.client); + this.pending.clear(); + } + + private scheduleCleanup(key: string): void { + const session = this.sessions.get(key); + if (!session) return; + session.cancelCleanup?.(); + session.cancelCleanup = this.schedule.after(IDLE_MS, () => { + session.cancelCleanup = undefined; + this.close(key); + }); + } +} diff --git a/plugins/host-metrics/src/backend/settings-routes.ts b/plugins/host-metrics/src/backend/settings-routes.ts deleted file mode 100644 index 1907886ee..000000000 --- a/plugins/host-metrics/src/backend/settings-routes.ts +++ /dev/null @@ -1,256 +0,0 @@ -import type { Express, RequestHandler } from "express"; -import { statsLogger } from "../../../../src/backend/utils/logger.js"; -import { createCurrentSettingsRepository } from "../../../../src/backend/database/repositories/factory.js"; - -type HostMetricsSettingsConfig = { - statusCheckInterval: number; - metricsInterval: number; -}; - -type HostMetricsSettingsRoutesDeps = { - requireAdmin: RequestHandler; - defaultStatsConfig: HostMetricsSettingsConfig; - refreshAllPolling: () => Promise; -}; - -function isMissingSettingsTable(error: unknown): boolean { - return ( - error instanceof Error && error.message.includes("no such table: settings") - ); -} - -export function registerHostMetricsSettingsRoutes( - app: Express, - { - requireAdmin, - defaultStatsConfig: DEFAULT_STATS_CONFIG, - refreshAllPolling, - }: HostMetricsSettingsRoutesDeps, -): void { - /** - * @openapi - * /global-settings: - * get: - * summary: Get global monitoring defaults - * tags: - * - Host Metrics - * responses: - * 200: - * description: Global monitoring settings. - * 403: - * description: Requires admin privileges. - */ - app.get("/global-settings", requireAdmin, async (_req, res) => { - try { - const settings = createCurrentSettingsRepository(); - const statusValue = await settings.get("global_status_check_interval"); - const metricsValue = await settings.get("global_metrics_interval"); - - res.json({ - statusCheckInterval: statusValue - ? parseInt(statusValue, 10) - : DEFAULT_STATS_CONFIG.statusCheckInterval, - metricsInterval: metricsValue - ? parseInt(metricsValue, 10) - : DEFAULT_STATS_CONFIG.metricsInterval, - }); - } catch (error) { - if (isMissingSettingsTable(error)) { - statsLogger.warn("Settings table does not exist, using defaults", { - operation: "global_settings_table_check", - error: error.message, - }); - return res.json({ - statusCheckInterval: DEFAULT_STATS_CONFIG.statusCheckInterval, - metricsInterval: DEFAULT_STATS_CONFIG.metricsInterval, - }); - } - - statsLogger.error("Failed to fetch global settings", { - operation: "global_settings_fetch_error", - error: error instanceof Error ? error.message : String(error), - }); - res.status(500).json({ error: "Failed to fetch global settings" }); - } - }); - - /** - * @openapi - * /global-settings: - * post: - * summary: Update global monitoring defaults - * tags: - * - Host Metrics - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * statusCheckInterval: - * type: integer - * metricsInterval: - * type: integer - * responses: - * 200: - * description: Settings saved. - * 400: - * description: Invalid parameters. - * 403: - * description: Requires admin privileges. - */ - app.post("/global-settings", requireAdmin, async (req, res) => { - const { statusCheckInterval, metricsInterval } = req.body; - - if ( - statusCheckInterval !== undefined && - (typeof statusCheckInterval !== "number" || - statusCheckInterval < 5 || - statusCheckInterval > 3600) - ) { - return res.status(400).json({ - error: "statusCheckInterval must be between 5 and 3600 seconds", - }); - } - if ( - metricsInterval !== undefined && - (typeof metricsInterval !== "number" || - metricsInterval < 5 || - metricsInterval > 3600) - ) { - return res - .status(400) - .json({ error: "metricsInterval must be between 5 and 3600 seconds" }); - } - - try { - const settings = createCurrentSettingsRepository(); - - if (statusCheckInterval !== undefined) { - await settings.set( - "global_status_check_interval", - String(statusCheckInterval), - ); - } - if (metricsInterval !== undefined) { - await settings.set("global_metrics_interval", String(metricsInterval)); - } - - await refreshAllPolling(); - - res.json({ - success: true, - message: "Settings updated and polling refreshed", - }); - } catch (error) { - if (isMissingSettingsTable(error)) { - statsLogger.error( - "Settings table does not exist, cannot save settings", - { - operation: "global_settings_table_check", - error: error.message, - }, - ); - return res.status(500).json({ - error: - "Database settings table is missing. Please check database initialization.", - }); - } - - statsLogger.error("Failed to save global settings", { - operation: "global_settings_save_error", - error: error instanceof Error ? error.message : String(error), - }); - res.status(500).json({ - error: "Failed to save global settings", - details: error instanceof Error ? error.message : String(error), - }); - } - }); - - /** - * @openapi - * /global-settings/history: - * get: - * summary: Get metrics history retention setting - * tags: - * - Host Metrics - * responses: - * 200: - * description: Retention setting in days. - * 403: - * description: Requires admin privileges. - */ - app.get("/global-settings/history", requireAdmin, async (_req, res) => { - try { - const value = await createCurrentSettingsRepository().get( - "metrics_history_retention_days", - ); - const days = value ? parseInt(value, 10) : 7; - res.json({ metricsHistoryRetentionDays: isNaN(days) ? 7 : days }); - } catch (error) { - statsLogger.error("Failed to fetch history retention setting", { - operation: "history_retention_fetch_error", - error: error instanceof Error ? error.message : String(error), - }); - res - .status(500) - .json({ error: "Failed to fetch history retention setting" }); - } - }); - - /** - * @openapi - * /global-settings/history: - * post: - * summary: Update metrics history retention setting - * tags: - * - Host Metrics - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * metricsHistoryRetentionDays: - * type: integer - * minimum: 1 - * maximum: 90 - * responses: - * 200: - * description: Setting saved. - * 400: - * description: Invalid value. - * 403: - * description: Requires admin privileges. - */ - app.post("/global-settings/history", requireAdmin, async (req, res) => { - const { metricsHistoryRetentionDays } = req.body; - if ( - typeof metricsHistoryRetentionDays !== "number" || - metricsHistoryRetentionDays < 1 || - metricsHistoryRetentionDays > 90 - ) { - return res.status(400).json({ - error: "metricsHistoryRetentionDays must be between 1 and 90", - }); - } - try { - await createCurrentSettingsRepository().set( - "metrics_history_retention_days", - String(Math.floor(metricsHistoryRetentionDays)), - ); - res.json({ success: true }); - } catch (error) { - statsLogger.error("Failed to save history retention setting", { - operation: "history_retention_save_error", - error: error instanceof Error ? error.message : String(error), - }); - res - .status(500) - .json({ error: "Failed to save history retention setting" }); - } - }); -} diff --git a/plugins/host-metrics/src/backend/ssh.ts b/plugins/host-metrics/src/backend/ssh.ts deleted file mode 100644 index b7a221543..000000000 --- a/plugins/host-metrics/src/backend/ssh.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { Client } from "ssh2"; -import type { - PluginSsh, - PluginSshConnection, - PluginSshConnectOptions, - PluginSshHost, -} from "@termix/plugin-sdk/backend"; - -let current: PluginSsh | null = null; - -/** Set in activate, cleared on deactivate. */ -export function setPluginSsh(ssh: PluginSsh | null): void { - current = ssh; -} - -export function pluginSsh(): PluginSsh { - if (!current) throw new Error("The plugin is not active"); - return current; -} - -/** withConnection with ssh2's Client type filled in. */ -export function withSshConnection( - host: number | PluginSshHost, - options: PluginSshConnectOptions & { pool: string }, - fn: (client: Client) => Promise, -): Promise { - return pluginSsh().withConnection(host, options, fn); -} - -/** connect with ssh2's Client type filled in. */ -export function connectSsh( - host: number | PluginSshHost, - options?: PluginSshConnectOptions, -): Promise> { - return pluginSsh().connect(host, options); -} diff --git a/src/backend/hosts/metrics-shared/state.ts b/plugins/host-metrics/src/backend/state.ts similarity index 90% rename from src/backend/hosts/metrics-shared/state.ts rename to plugins/host-metrics/src/backend/state.ts index 9cec72c49..e51bfd1ed 100644 --- a/src/backend/hosts/metrics-shared/state.ts +++ b/plugins/host-metrics/src/backend/state.ts @@ -1,4 +1,6 @@ -class RequestQueue { +export type HostStatus = "online" | "reachable" | "offline"; + +export class RequestQueue { private queues = new Map Promise>>(); private processing = new Set(); private requestTimeout = 60000; @@ -65,7 +67,7 @@ interface CachedMetrics { hostId: number; } -class MetricsCache { +export class MetricsCache { private cache = new Map(); private ttl = 30000; @@ -101,7 +103,7 @@ interface AuthFailureRecord { permanent: boolean; } -class AuthFailureTracker { +export class AuthFailureTracker { private failures = new Map(); private maxRetries = 3; private backoffBase = 5000; @@ -196,7 +198,7 @@ class AuthFailureTracker { } } -class PollingBackoff { +export class PollingBackoff { private failures = new Map(); private baseDelay = 30000; private maxDelay = 600000; @@ -260,7 +262,7 @@ class PollingBackoff { /** * Limits how many async jobs run at once. Extra callers wait in FIFO order. - * Used to stop host-metrics status/metrics polls from stampeding under load. + * Used to stop metrics polls from stampeding under load. */ export class ConcurrentLimiter { private active = 0; @@ -408,27 +410,30 @@ export class HostPollCache { } } -/** TCP status checks are cheap relative to SSH metrics collection. */ -export const statusPollLimiter = new ConcurrentLimiter(20); -/** SSH metrics execs are expensive; keep concurrency tight. */ -export const metricsPollLimiter = new ConcurrentLimiter(5); -/** Viewer registration is bursty; admit only two first samples at a time. */ -export const initialMetricsPollLimiter = new ConcurrentLimiter(2); - +/** + * The first heavy SSH sample waits until core's cheap status check says the + * host answers. A host core does not check (status checks off) is tried. + */ export function canStartInitialMetrics( - status: HostStatus | undefined, + status: HostStatus | null | undefined, hasViewers: boolean, - statusCheckEnabled = true, ): boolean { - return ( - hasViewers && - (!statusCheckEnabled || status === "reachable" || status === "online") - ); + return hasViewers && status !== "offline"; } -export const hostPollCache = new HostPollCache(30_000); -export const requestQueue = new RequestQueue(); -export const metricsCache = new MetricsCache(); -export const authFailureTracker = new AuthFailureTracker(); -export const pollingBackoff = new PollingBackoff(); -import type { HostStatus } from "./host-status.js"; +/** Everything one activation of the plugin polls with. */ +export function createMetricsState() { + return { + /** SSH metrics execs are expensive; resized with the fleet. */ + metricsLimiter: new ConcurrentLimiter(5), + /** Viewer registration is bursty; admit only two first samples at a time. */ + initialLimiter: new ConcurrentLimiter(2), + hostCache: new HostPollCache(30_000), + requestQueue: new RequestQueue(), + metricsCache: new MetricsCache(), + authFailures: new AuthFailureTracker(), + backoff: new PollingBackoff(), + }; +} + +export type MetricsState = ReturnType; diff --git a/plugins/host-metrics/src/backend/tables.ts b/plugins/host-metrics/src/backend/tables.ts new file mode 100644 index 000000000..9623438db --- /dev/null +++ b/plugins/host-metrics/src/backend/tables.ts @@ -0,0 +1,120 @@ +import { + adoptLegacyTable, + boolean, + defineTable, + id, + integer, + real, + refHost, + refUser, + text, + timestamp, + varchar, +} from "@termix/plugin-sdk/db"; + +/** One card layout per user per host, as JSON. */ +export const hostMetricsPreferences = adoptLegacyTable( + "host_metrics_preferences", + defineTable( + "host_metrics_preferences", + { + id: id(), + userId: refUser(), + hostId: refHost(), + layout: text().notNull(), + createdAt: timestamp().notNull().defaultNow(), + updatedAt: timestamp().notNull().defaultNow(), + }, + { + uniques: [ + { + name: "idx_host_metrics_prefs_user_host", + columns: ["userId", "hostId"], + }, + ], + }, + ), +); + +/** A user's health checks for a host, as a JSON array. */ +export const hostHealthChecks = adoptLegacyTable( + "host_health_checks", + defineTable( + "host_health_checks", + { + id: id(), + userId: refUser(), + hostId: refHost(), + checks: text().notNull(), + intervalSeconds: integer().notNull().default(300), + createdAt: timestamp().notNull().defaultNow(), + updatedAt: timestamp().notNull().defaultNow(), + }, + { + uniques: [ + { + name: "idx_host_health_checks_user_host", + columns: ["userId", "hostId"], + }, + ], + }, + ), +); + +export const hostHealthHistory = adoptLegacyTable( + "host_health_history", + defineTable( + "host_health_history", + { + id: id(), + userId: refUser(), + hostId: refHost(), + checkId: varchar().notNull(), + ts: timestamp().notNull().defaultNow(), + ok: boolean().notNull(), + latencyMs: integer(), + detail: text(), + }, + { + indexes: [ + { + name: "idx_host_health_history_lookup", + columns: ["userId", "hostId", "checkId", "ts"], + }, + ], + }, + ), +); + +/** One row per metrics sample, pruned to the retention setting. */ +export const hostMetricsHistory = adoptLegacyTable( + "host_metrics_history", + defineTable( + "host_metrics_history", + { + id: id(), + hostId: refHost(), + ts: timestamp().notNull().defaultNow(), + cpuPercent: real(), + memPercent: real(), + diskPercent: real(), + netRxBytes: integer(), + netTxBytes: integer(), + }, + { + indexes: [ + { + name: "idx_host_metrics_history_host_ts", + columns: ["hostId", "ts"], + }, + ], + }, + ), +); + +export const tables = [ + hostMetricsPreferences, + hostHealthChecks, + hostHealthHistory, + hostMetricsHistory, +]; diff --git a/plugins/host-metrics/src/backend/util.ts b/plugins/host-metrics/src/backend/util.ts new file mode 100644 index 000000000..1a0fb6a48 --- /dev/null +++ b/plugins/host-metrics/src/backend/util.ts @@ -0,0 +1,17 @@ +export function toFixedNum( + n: number | null | undefined, + digits = 2, +): number | null { + if (typeof n !== "number" || !Number.isFinite(n)) return null; + return Number(n.toFixed(digits)); +} + +export function kibToGiB(kib: number): number { + return kib / (1024 * 1024); +} + +export function errorMessage(error: unknown, fallback = "Unknown error") { + if (error instanceof Error && error.message) return error.message; + if (typeof error === "string" && error) return error; + return fallback; +} diff --git a/plugins/host-metrics/src/backend/viewer-routes.ts b/plugins/host-metrics/src/backend/viewer-routes.ts deleted file mode 100644 index cf96ce8ab..000000000 --- a/plugins/host-metrics/src/backend/viewer-routes.ts +++ /dev/null @@ -1,291 +0,0 @@ -import { getErrorMessage } from "../../../../src/backend/utils/error-message.js"; -import type { Express } from "express"; -import type { AuthenticatedRequest } from "../../../../src/types/index.js"; -import { statsLogger } from "../../../../src/backend/utils/logger.js"; -import { DataCrypto } from "../../../../src/backend/utils/data-crypto.js"; - -type ViewerStatsConfig = { - metricsEnabled: boolean; -}; - -type HostMetricsViewerRoutesDeps< - THost extends { statsConfig?: string | TStatsConfig }, - TStatsConfig extends ViewerStatsConfig, -> = { - fetchHostById: (hostId: number, userId: string) => Promise; - supportsMetrics: (host: THost) => boolean; - parseStatsConfig: (statsConfig: THost["statsConfig"]) => TStatsConfig; - updateHeartbeat: (viewerSessionId: string) => boolean; - registerViewer: ( - hostId: number, - viewerSessionId: string, - userId: string, - ) => void; - unregisterViewer: (hostId: number, viewerSessionId: string) => void; - /** Route path segment, e.g. "metrics" -> /metrics/heartbeat. Defaults to "metrics". */ - pathPrefix?: string; -}; - -export function registerHostMetricsViewerRoutes< - THost extends { statsConfig?: string | TStatsConfig }, - TStatsConfig extends ViewerStatsConfig, ->( - app: Express, - { - fetchHostById, - supportsMetrics, - parseStatsConfig, - updateHeartbeat, - registerViewer, - unregisterViewer, - pathPrefix = "metrics", - }: HostMetricsViewerRoutesDeps, -): void { - /** - * @openapi - * /metrics/heartbeat: - * post: - * summary: Update viewer heartbeat - * description: Updates the heartbeat timestamp for a metrics viewer session to keep it alive. - * tags: - * - Host Metrics - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * viewerSessionId: - * type: string - * responses: - * 200: - * description: Heartbeat updated successfully. - * 400: - * description: Invalid viewerSessionId. - * 401: - * description: Session expired - please log in again. - * 404: - * description: Viewer session not found. - * 500: - * description: Failed to update heartbeat. - */ - app.post(`/${pathPrefix}/heartbeat`, async (req, res) => { - const { viewerSessionId } = req.body; - const userId = (req as AuthenticatedRequest).userId; - - if (DataCrypto.getUserDataKey(userId) === null) { - return res.status(401).json({ - error: "Session expired - please log in again", - code: "SESSION_EXPIRED", - }); - } - - if (!viewerSessionId || typeof viewerSessionId !== "string") { - return res.status(400).json({ error: "Invalid viewerSessionId" }); - } - - try { - const success = updateHeartbeat(viewerSessionId); - if (success) { - res.json({ success: true }); - } else { - res.status(404).json({ error: "Viewer session not found" }); - } - } catch (error) { - statsLogger.error("Failed to update heartbeat", { - operation: "heartbeat_error", - viewerSessionId, - error: error instanceof Error ? error.message : String(error), - }); - res.status(500).json({ error: "Failed to update heartbeat" }); - } - }); - - /** - * @openapi - * /metrics/register-viewer: - * post: - * summary: Register metrics viewer - * description: Registers a new viewer session for a host to track who is viewing metrics. - * tags: - * - Host Metrics - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * hostId: - * type: integer - * responses: - * 200: - * description: Viewer registered successfully. - * 400: - * description: Invalid hostId. - * 401: - * description: Session expired - please log in again. - * 500: - * description: Failed to register viewer. - */ - app.post(`/${pathPrefix}/register-viewer`, async (req, res) => { - const { hostId } = req.body; - const userId = (req as AuthenticatedRequest).userId; - - if (DataCrypto.getUserDataKey(userId) === null) { - return res.status(401).json({ - error: "Session expired - please log in again", - code: "SESSION_EXPIRED", - }); - } - - if (!hostId || typeof hostId !== "number") { - return res.status(400).json({ error: "Invalid hostId" }); - } - - try { - // Graceful no-op if host is inaccessible, metrics disabled, or host type - // does not support metrics. The client may call this speculatively, so - // avoid returning 5xx for expected "no metrics available" scenarios. - let host: THost | undefined; - try { - host = await fetchHostById(hostId, userId); - } catch (lookupErr) { - statsLogger.warn( - "register-viewer host lookup failed (treating as no-op)", - { - operation: "register_viewer_lookup", - hostId, - userId, - error: getErrorMessage(lookupErr, String(lookupErr)), - }, - ); - } - - if (!host) { - return res.json({ - success: true, - skipped: true, - reason: "host_not_found", - }); - } - - if (!supportsMetrics(host)) { - return res.json({ - success: true, - skipped: true, - reason: "metrics_unsupported", - }); - } - - const statsConfig = parseStatsConfig(host.statsConfig); - if (!statsConfig.metricsEnabled) { - return res.json({ - success: true, - skipped: true, - reason: "metrics_disabled", - }); - } - - const viewerSessionId = `viewer-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; - try { - registerViewer(hostId, viewerSessionId, userId); - } catch (regErr) { - statsLogger.warn( - "pollingManager.registerViewer threw (treating as no-op)", - { - operation: "register_viewer_internal", - hostId, - userId, - error: regErr instanceof Error ? regErr.message : String(regErr), - }, - ); - return res.json({ - success: true, - skipped: true, - reason: "register_failed_noop", - }); - } - - res.json({ success: true, viewerSessionId }); - } catch (error) { - statsLogger.error("Failed to register viewer", { - operation: "register_viewer_error", - hostId, - userId, - error: error instanceof Error ? error.message : String(error), - }); - // Even on unexpected errors we prefer a graceful client experience: the - // viewer-registration is purely an optimization and should never break - // the UI. Report success:false but HTTP 200 so the client can decide. - res.status(200).json({ - success: false, - skipped: true, - reason: "internal_error", - }); - } - }); - - /** - * @openapi - * /metrics/unregister-viewer: - * post: - * summary: Unregister metrics viewer - * description: Unregisters a viewer session when they stop viewing metrics for a host. - * tags: - * - Host Metrics - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * hostId: - * type: integer - * viewerSessionId: - * type: string - * responses: - * 200: - * description: Viewer unregistered successfully. - * 400: - * description: Invalid hostId or viewerSessionId. - * 401: - * description: Session expired - please log in again. - * 500: - * description: Failed to unregister viewer. - */ - app.post(`/${pathPrefix}/unregister-viewer`, async (req, res) => { - const { hostId, viewerSessionId } = req.body; - const userId = (req as AuthenticatedRequest).userId; - - if (DataCrypto.getUserDataKey(userId) === null) { - return res.status(401).json({ - error: "Session expired - please log in again", - code: "SESSION_EXPIRED", - }); - } - - if (!hostId || typeof hostId !== "number") { - return res.status(400).json({ error: "Invalid hostId" }); - } - - if (!viewerSessionId || typeof viewerSessionId !== "string") { - return res.status(400).json({ error: "Invalid viewerSessionId" }); - } - - try { - unregisterViewer(hostId, viewerSessionId); - res.json({ success: true }); - } catch (error) { - statsLogger.error("Failed to unregister viewer", { - operation: "unregister_viewer_error", - hostId, - viewerSessionId, - error: error instanceof Error ? error.message : String(error), - }); - res.status(500).json({ error: "Failed to unregister viewer" }); - } - }); -} diff --git a/plugins/host-metrics/src/backend/widgets/cpu-collector.ts b/plugins/host-metrics/src/backend/widgets/cpu-collector.ts index 1bc999f45..f14ea6cf7 100644 --- a/plugins/host-metrics/src/backend/widgets/cpu-collector.ts +++ b/plugins/host-metrics/src/backend/widgets/cpu-collector.ts @@ -1,10 +1,10 @@ -import type { Client } from "ssh2"; import { execCommand, execPowerShell, - toFixedNum, type HostPlatform, -} from "../../../../../src/backend/hosts/metrics-shared/common-utils.js"; +} from "@termix/plugin-sdk/host-commands"; +import { toFixedNum } from "../util.js"; +import type { Client } from "ssh2"; /** * CPU percent is derived from the delta against the previous poll's diff --git a/plugins/host-metrics/src/backend/widgets/disk-collector.ts b/plugins/host-metrics/src/backend/widgets/disk-collector.ts index 1e5d72562..834bb69b8 100644 --- a/plugins/host-metrics/src/backend/widgets/disk-collector.ts +++ b/plugins/host-metrics/src/backend/widgets/disk-collector.ts @@ -1,10 +1,10 @@ -import type { Client } from "ssh2"; import { execCommand, execPowerShell, - toFixedNum, type HostPlatform, -} from "../../../../../src/backend/hosts/metrics-shared/common-utils.js"; +} from "@termix/plugin-sdk/host-commands"; +import { toFixedNum } from "../util.js"; +import type { Client } from "ssh2"; const PSEUDO_FS_RE = /^(tmpfs|devtmpfs|overlay|udev|none|shm)$/; diff --git a/plugins/host-metrics/src/backend/widgets/firewall-collector.ts b/plugins/host-metrics/src/backend/widgets/firewall-collector.ts index 0ed13ffa2..7c5ebb14d 100644 --- a/plugins/host-metrics/src/backend/widgets/firewall-collector.ts +++ b/plugins/host-metrics/src/backend/widgets/firewall-collector.ts @@ -1,10 +1,10 @@ +import { execCommand } from "@termix/plugin-sdk/host-commands"; import type { Client } from "ssh2"; -import { execCommand } from "../../../../../src/backend/hosts/metrics-shared/common-utils.js"; import type { FirewallMetrics, FirewallChain, FirewallRule, -} from "../../../../../src/types/stats-widgets.js"; +} from "../../shared/stats-widgets.js"; function parseIptablesRule(line: string): FirewallRule | null { if (!line.startsWith("-A ")) return null; diff --git a/plugins/host-metrics/src/backend/widgets/gpu-collector.ts b/plugins/host-metrics/src/backend/widgets/gpu-collector.ts index 9e6b6daa7..14b2a899c 100644 --- a/plugins/host-metrics/src/backend/widgets/gpu-collector.ts +++ b/plugins/host-metrics/src/backend/widgets/gpu-collector.ts @@ -1,8 +1,6 @@ +import { execCommand } from "@termix/plugin-sdk/host-commands"; +import { toFixedNum } from "../util.js"; import type { Client } from "ssh2"; -import { - execCommand, - toFixedNum, -} from "../../../../../src/backend/hosts/metrics-shared/common-utils.js"; export interface GpuDevice { index: number; diff --git a/plugins/host-metrics/src/backend/widgets/login-stats-collector.ts b/plugins/host-metrics/src/backend/widgets/login-stats-collector.ts index afc992169..7850f9421 100644 --- a/plugins/host-metrics/src/backend/widgets/login-stats-collector.ts +++ b/plugins/host-metrics/src/backend/widgets/login-stats-collector.ts @@ -1,5 +1,5 @@ +import { execCommand } from "@termix/plugin-sdk/host-commands"; import type { Client } from "ssh2"; -import { execCommand } from "../../../../../src/backend/hosts/metrics-shared/common-utils.js"; export interface LoginRecord { user: string; diff --git a/plugins/host-metrics/src/backend/widgets/memory-collector.ts b/plugins/host-metrics/src/backend/widgets/memory-collector.ts index c34d98e03..59fb96da3 100644 --- a/plugins/host-metrics/src/backend/widgets/memory-collector.ts +++ b/plugins/host-metrics/src/backend/widgets/memory-collector.ts @@ -1,11 +1,10 @@ -import type { Client } from "ssh2"; import { execCommand, execPowerShell, - toFixedNum, - kibToGiB, type HostPlatform, -} from "../../../../../src/backend/hosts/metrics-shared/common-utils.js"; +} from "@termix/plugin-sdk/host-commands"; +import { toFixedNum, kibToGiB } from "../util.js"; +import type { Client } from "ssh2"; async function collectDarwinMemoryMetrics(client: Client): Promise<{ percent: number | null; diff --git a/plugins/host-metrics/src/backend/widgets/network-collector.ts b/plugins/host-metrics/src/backend/widgets/network-collector.ts index d4ef36435..ca21628b2 100644 --- a/plugins/host-metrics/src/backend/widgets/network-collector.ts +++ b/plugins/host-metrics/src/backend/widgets/network-collector.ts @@ -1,9 +1,9 @@ -import type { Client } from "ssh2"; import { execCommand, execPowerShell, type HostPlatform, -} from "../../../../../src/backend/hosts/metrics-shared/common-utils.js"; +} from "@termix/plugin-sdk/host-commands"; +import type { Client } from "ssh2"; export interface NetworkCounters { rx: string; diff --git a/plugins/host-metrics/src/backend/widgets/ports-collector.ts b/plugins/host-metrics/src/backend/widgets/ports-collector.ts index a467bfc3a..c137fa774 100644 --- a/plugins/host-metrics/src/backend/widgets/ports-collector.ts +++ b/plugins/host-metrics/src/backend/widgets/ports-collector.ts @@ -1,9 +1,9 @@ +import { execCommand } from "@termix/plugin-sdk/host-commands"; import type { Client } from "ssh2"; -import { execCommand } from "../../../../../src/backend/hosts/metrics-shared/common-utils.js"; import type { PortsMetrics, ListeningPort, -} from "../../../../../src/types/stats-widgets.js"; +} from "../../shared/stats-widgets.js"; function parseSsOutput(output: string): ListeningPort[] { const ports: ListeningPort[] = []; diff --git a/plugins/host-metrics/src/backend/widgets/processes-collector.ts b/plugins/host-metrics/src/backend/widgets/processes-collector.ts index 89d8f1dc2..4699b7df1 100644 --- a/plugins/host-metrics/src/backend/widgets/processes-collector.ts +++ b/plugins/host-metrics/src/backend/widgets/processes-collector.ts @@ -1,5 +1,5 @@ +import { execCommand } from "@termix/plugin-sdk/host-commands"; import type { Client } from "ssh2"; -import { execCommand } from "../../../../../src/backend/hosts/metrics-shared/common-utils.js"; export async function collectProcessesMetrics(client: Client): Promise<{ total: number | null; diff --git a/plugins/host-metrics/src/backend/widgets/system-collector.ts b/plugins/host-metrics/src/backend/widgets/system-collector.ts index ff5677bfc..c3d04fe38 100644 --- a/plugins/host-metrics/src/backend/widgets/system-collector.ts +++ b/plugins/host-metrics/src/backend/widgets/system-collector.ts @@ -1,9 +1,9 @@ -import type { Client } from "ssh2"; import { execCommand, execPowerShell, type HostPlatform, -} from "../../../../../src/backend/hosts/metrics-shared/common-utils.js"; +} from "@termix/plugin-sdk/host-commands"; +import type { Client } from "ssh2"; async function collectWindowsSystemMetrics(client: Client): Promise<{ hostname: string | null; diff --git a/plugins/host-metrics/src/backend/widgets/temperature-collector.ts b/plugins/host-metrics/src/backend/widgets/temperature-collector.ts index 2adc09f8f..c595373a8 100644 --- a/plugins/host-metrics/src/backend/widgets/temperature-collector.ts +++ b/plugins/host-metrics/src/backend/widgets/temperature-collector.ts @@ -1,8 +1,6 @@ +import { execCommand } from "@termix/plugin-sdk/host-commands"; +import { toFixedNum } from "../util.js"; import type { Client } from "ssh2"; -import { - execCommand, - toFixedNum, -} from "../../../../../src/backend/hosts/metrics-shared/common-utils.js"; export interface TemperatureSensor { label: string; diff --git a/plugins/host-metrics/src/backend/widgets/uptime-collector.ts b/plugins/host-metrics/src/backend/widgets/uptime-collector.ts index 23b4a4439..e289ae5d1 100644 --- a/plugins/host-metrics/src/backend/widgets/uptime-collector.ts +++ b/plugins/host-metrics/src/backend/widgets/uptime-collector.ts @@ -1,9 +1,9 @@ -import type { Client } from "ssh2"; import { execCommand, execPowerShell, type HostPlatform, -} from "../../../../../src/backend/hosts/metrics-shared/common-utils.js"; +} from "@termix/plugin-sdk/host-commands"; +import type { Client } from "ssh2"; function formatUptime(uptimeSeconds: number): string { const days = Math.floor(uptimeSeconds / 86400); diff --git a/plugins/host-metrics/src/frontend/HostEditorStatsTab.tsx b/plugins/host-metrics/src/frontend/HostEditorStatsTab.tsx index 066d311d0..207d896d4 100644 --- a/plugins/host-metrics/src/frontend/HostEditorStatsTab.tsx +++ b/plugins/host-metrics/src/frontend/HostEditorStatsTab.tsx @@ -1,7 +1,18 @@ -import { useState } from "react"; -import { useTranslation } from "@termix/plugin-sdk/frontend"; import { - Activity, + Button, + Input, + SectionCard, + SettingRow, + FakeSwitch, + Select2, +} from "@termix/plugin-sdk/ui"; +import { useEffect, useState } from "react"; +import { + usePluginApi, + useTranslation, + type HostEditorSectionProps, +} from "@termix/plugin-sdk/frontend"; +import { HardDrive, LayoutDashboard, Plus, @@ -10,32 +21,66 @@ import { Zap, } from "lucide-react"; -import { Button } from "@/components/button"; -import { Input } from "@/components/input"; -import { SectionCard, SettingRow, FakeSwitch } from "@/components/section-card"; -import type { HostEditorForm } from "@/sidebar/HostEditorData"; -import { Select2 } from "@/components/select2"; +import { readHostMetricsSettings } from "../shared/stats-widgets.js"; -type SetHostField = ( - key: K, - value: HostEditorForm[K], -) => void; +const PLUGIN_ID = "host-metrics"; +type PluginSettingsBag = Record>; +type QuickActionRow = { name: string; snippetId: string }; + +/** + * The Host Metrics section of the host editor. Its values are this plugin's + * host settings, carried on form.pluginSettings and saved by the editor after + * the host itself. Quick actions are a core host field. + */ export function HostStatsTab({ form, setField, + updateForm, + host, snippets, -}: { - form: HostEditorForm; - setField: SetHostField; - snippets: { id: number; name: string }[]; -}) { +}: HostEditorSectionProps) { const { t } = useTranslation(); const [newMount, setNewMount] = useState(""); const [newMonitoredPath, setNewMonitoredPath] = useState(""); const [newMonitoredLabel, setNewMonitoredLabel] = useState(""); - const excludedMounts = form.statsConfig.excludedMounts ?? []; - const monitoredMounts = form.statsConfig.monitoredMounts ?? []; + const bag = (form.pluginSettings as PluginSettingsBag | undefined)?.[ + PLUGIN_ID + ]; + const settings = readHostMetricsSettings(bag); + const { excludedMounts, monitoredMounts } = settings; + const quickActions = (form.quickActions ?? []) as QuickActionRow[]; + const snippetOptions = (snippets ?? []) as { id: number; name: string }[]; + const api = usePluginApi(); + + const patch = (values: Record) => + updateForm((current) => { + const all = + (current.pluginSettings as PluginSettingsBag | undefined) ?? {}; + return { + pluginSettings: { + ...all, + [PLUGIN_ID]: { ...(all[PLUGIN_ID] ?? {}), ...values }, + }, + }; + }); + + // A new host starts with the admin's default for metrics. + useEffect(() => { + if (host || bag?.metricsEnabled !== undefined) return; + let cancelled = false; + api + .get<{ enabledForNewHosts: boolean }>("/defaults") + .then(({ data }) => { + if (!cancelled && data.enabledForNewHosts === false) { + patch({ metricsEnabled: false }); + } + }) + .catch(() => {}); + return () => { + cancelled = true; + }; + }, [host]); const addExcludedMount = () => { const value = newMount.trim(); @@ -43,18 +88,14 @@ export function HostStatsTab({ setNewMount(""); return; } - setField("statsConfig", { - ...form.statsConfig, - excludedMounts: [...excludedMounts, value], - }); + patch({ excludedMounts: [...excludedMounts, value] }); setNewMount(""); }; const addMonitoredMount = () => { const path = newMonitoredPath.trim(); if (!path || monitoredMounts.some((entry) => entry.path === path)) return; - setField("statsConfig", { - ...form.statsConfig, + patch({ monitoredMounts: [ ...monitoredMounts, { path, label: newMonitoredLabel.trim() || undefined }, @@ -66,62 +107,6 @@ export function HostStatsTab({ return ( <> - } - > -
- - - setField("statsConfig", { - ...form.statsConfig, - statusCheckEnabled: v, - }) - } - /> - - {form.statsConfig.statusCheckEnabled && ( - - - setField("statsConfig", { - ...form.statsConfig, - useGlobalStatusInterval: v, - }) - } - /> - - )} - {form.statsConfig.statusCheckEnabled && - !form.statsConfig.useGlobalStatusInterval && ( - - - setField("statsConfig", { - ...form.statsConfig, - statusCheckInterval: Number(e.target.value), - }) - } - className="w-20 h-7 text-xs text-right [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" - /> - - )} -
-
} @@ -132,50 +117,36 @@ export function HostStatsTab({ description={t("hosts.enableMetricsDesc")} > - setField("statsConfig", { - ...form.statsConfig, - metricsEnabled: v, - }) - } + checked={settings.metricsEnabled} + onChange={(v) => patch({ metricsEnabled: v })} /> - {form.statsConfig.metricsEnabled && ( + {settings.metricsEnabled && ( - setField("statsConfig", { - ...form.statsConfig, - useGlobalMetricsInterval: v, - }) + checked={settings.metricsInterval === null} + onChange={(v) => patch({ metricsInterval: v ? null : 30 })} + /> + + )} + {settings.metricsEnabled && settings.metricsInterval !== null && ( + + + patch({ metricsInterval: Number(e.target.value) }) } + className="w-20 h-7 text-xs text-right [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" /> )} - {form.statsConfig.metricsEnabled && - !form.statsConfig.useGlobalMetricsInterval && ( - - - setField("statsConfig", { - ...form.statsConfig, - metricsInterval: Number(e.target.value), - }) - } - className="w-20 h-7 text-xs text-right [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" - /> - - )} - setField("statsConfig", { - ...form.statsConfig, + patch({ monitoredMounts: monitoredMounts.filter( (mount) => mount.path !== entry.path, ), @@ -295,8 +265,7 @@ export function HostStatsTab({ - - - {t("admin.metricsHistoryRetentionRange")} - - - ); -} diff --git a/plugins/host-metrics/src/frontend/SummaryViews.tsx b/plugins/host-metrics/src/frontend/SummaryViews.tsx new file mode 100644 index 000000000..4cc0f8d3c --- /dev/null +++ b/plugins/host-metrics/src/frontend/SummaryViews.tsx @@ -0,0 +1,269 @@ +import { usePermission, useTranslation } from "@termix/plugin-sdk/frontend"; +import type { MetricsSummaryStore } from "./summary-store"; +import { useMetricsSummary } from "./summary-store"; + +type MetricKey = + "cpu" | "memory" | "disk" | "uptime" | "system" | "network" | "processes"; + +function RowBar({ label, value }: { label: string; value: number }) { + const tone = + value >= 90 + ? ["bg-red-500", "text-red-400"] + : value >= 70 + ? ["bg-yellow-500", "text-yellow-400"] + : ["bg-accent-brand", "text-accent-brand"]; + return ( +
+
+ {label} + + {value.toFixed(0)}% + +
+
+
+
+
+ ); +} + +/** CPU, RAM and disk bars in a dashboard host row ("dashboard.hostMetrics"). */ +export function createDashboardHostMetrics(store: MetricsSummaryStore) { + return function DashboardHostMetrics(props: Record) { + const { t } = useTranslation(); + const allowed = usePermission("use"); + const hostId = Number(props.hostId) || null; + const online = props.online === true; + const metrics = useMetricsSummary(store, allowed && online ? hostId : null); + + const cpu = metrics?.cpu?.percent ?? null; + const ram = metrics?.memory?.percent ?? null; + const disk = metrics?.disk?.percent ?? null; + if (!allowed) return null; + if (!online || (cpu === null && ram === null && disk === null)) { + return ( +
+ {[0, 1, 2].map((slot) => ( + + - + + ))} +
+ ); + } + return ( +
+ {cpu !== null && } + {ram !== null && } + {disk !== null && ( + + )} +
+ ); + }; +} + +function accentColor(): string { + return ( + getComputedStyle(document.documentElement) + .getPropertyValue("--accent-brand") + .trim() || "#f59145" + ); +} + +function WidgetBar({ + label, + value, + sublabel, +}: { + label: string; + value: number | null; + sublabel?: string; +}) { + const pct = value ?? 0; + const color = pct > 90 ? "#ef4444" : pct > 70 ? "#f97316" : accentColor(); + return ( +
+
+ {label} + + {value != null ? `${Math.round(pct)}%` : "N/A"} + {sublabel && {sublabel}} + +
+
+
+
+
+ ); +} + +function InfoRow({ + label, + value, +}: { + label: string; + value: string | null | undefined; +}) { + if (!value) return null; + return ( +
+ {label} + {value} +
+ ); +} + +function formatBytes(n: number): string { + if (n >= 1073741824) return `${(n / 1073741824).toFixed(1)} GB`; + if (n >= 1048576) return `${(n / 1048576).toFixed(1)} MB`; + if (n >= 1024) return `${(n / 1024).toFixed(1)} KB`; + return `${n} B`; +} + +/** The numbers under core's homepage host status widget ("homepage.hostMetrics"). */ +export function createHomepageHostMetrics(store: MetricsSummaryStore) { + return function HomepageHostMetrics(props: Record) { + const { t } = useTranslation(); + const allowed = usePermission("use"); + const hostId = Number(props.hostId) || null; + const shown = ( + Array.isArray(props.shownMetrics) ? props.shownMetrics : [] + ) as MetricKey[]; + const online = props.online === true; + const metrics = useMetricsSummary(store, allowed ? hostId : null); + if (!allowed || shown.length === 0) return null; + + const interfaces = metrics?.network?.interfaces ?? []; + const sum = (pick: (iface: (typeof interfaces)[number]) => unknown) => + interfaces.reduce((total, iface) => { + const raw = pick(iface); + return raw ? total + parseFloat(String(raw)) : total; + }, 0); + const totalRx = sum((iface) => iface.rxBytes); + const totalTx = sum((iface) => iface.txBytes); + + return ( +
+ {shown.includes("cpu") && ( + + )} + {shown.includes("memory") && ( + + )} + {shown.includes("disk") && ( + + )} + {shown.includes("uptime") && ( + + )} + {shown.includes("system") && metrics?.system && ( +
+ + + +
+ )} + {shown.includes("network") && interfaces.length > 0 && ( +
+
+ {t("homepage.metricNetwork")} +
+ {interfaces.slice(0, 3).map((iface) => ( +
+ + {iface.name} + + + {iface.ip || iface.state} + +
+ ))} + {(totalRx > 0 || totalTx > 0) && ( +
+ RX/TX + + {formatBytes(totalRx)} / {formatBytes(totalTx)} + +
+ )} +
+ )} + {shown.includes("processes") && metrics?.processes && ( +
+
+ + {t("homepage.metricProcesses")} + + + {metrics.processes.total ?? "?"}{" "} + {t("homepage.metricProcessesTotal")} + {metrics.processes.running != null + ? `, ${metrics.processes.running} ${t("homepage.metricProcessesRunning")}` + : ""} + +
+ {metrics.processes.top?.slice(0, 3).map((proc) => ( +
+ + {proc.command} + + {proc.cpu}% +
+ ))} +
+ )} + {!metrics && online && ( + + {t("homepage.metricsNotAvailable")} + + )} +
+ ); + }; +} diff --git a/plugins/host-metrics/src/frontend/cards/CardTimeTabs.tsx b/plugins/host-metrics/src/frontend/cards/CardTimeTabs.tsx index fbd0f88ec..0b5691d68 100644 --- a/plugins/host-metrics/src/frontend/cards/CardTimeTabs.tsx +++ b/plugins/host-metrics/src/frontend/cards/CardTimeTabs.tsx @@ -1,4 +1,4 @@ -import { cn } from "@/lib/utils"; +import { cn } from "@termix/plugin-sdk/ui"; import { useTranslation } from "@termix/plugin-sdk/frontend"; export type HistoryTab = "live" | "1h" | "6h" | "24h" | "7d"; diff --git a/plugins/host-metrics/src/frontend/cards/CpuCard.tsx b/plugins/host-metrics/src/frontend/cards/CpuCard.tsx index 01b280505..784b70dd6 100644 --- a/plugins/host-metrics/src/frontend/cards/CpuCard.tsx +++ b/plugins/host-metrics/src/frontend/cards/CpuCard.tsx @@ -1,11 +1,16 @@ +import { + RadialGauge, + Sparkline, + MiniStat, + MetricCard, + LineChart, + type LineChartSeries, +} from "@termix/plugin-sdk/ui"; +import { type ServerMetrics } from "../../shared/metrics.js"; import { useState, useEffect } from "react"; import { Cpu } from "lucide-react"; import { useTranslation } from "@termix/plugin-sdk/frontend"; -import type { ServerMetrics } from "@/main-axios"; -import { RadialGauge, Sparkline, MiniStat } from "@/components/charts"; -import { MetricCard } from "@/components/metric-card"; -import { LineChart, type LineChartSeries } from "@/components/charts/LineChart"; -import { getMetricsHistory, type MetricsHistoryRow } from "../host-metrics-api"; +import { type MetricsHistoryRow, useHostMetricsApi } from "../host-metrics-api"; import { CardTimeTabs, type HistoryTab } from "./CardTimeTabs"; function computeChartData(rows: MetricsHistoryRow[]) { @@ -24,6 +29,7 @@ export function CpuCard({ history: number[]; hostId: number | null; }) { + const { getMetricsHistory } = useHostMetricsApi(); const { t } = useTranslation(); const [activeTab, setActiveTab] = useState("live"); const [rows, setRows] = useState(null); diff --git a/plugins/host-metrics/src/frontend/cards/DiskCard.tsx b/plugins/host-metrics/src/frontend/cards/DiskCard.tsx index c1bb8ca0b..68dd25367 100644 --- a/plugins/host-metrics/src/frontend/cards/DiskCard.tsx +++ b/plugins/host-metrics/src/frontend/cards/DiskCard.tsx @@ -1,12 +1,17 @@ +import { + RadialGauge, + Sparkline, + MiniStat, + MetricCard, + LineChart, + type LineChartSeries, +} from "@termix/plugin-sdk/ui"; +import { type ServerMetrics } from "../../shared/metrics.js"; import { useState, useEffect, useMemo } from "react"; import { HardDrive } from "lucide-react"; import { useTranslation } from "@termix/plugin-sdk/frontend"; -import type { ServerMetrics } from "@/main-axios"; import { FilesystemPicker } from "./FilesystemPicker"; -import { RadialGauge, Sparkline, MiniStat } from "@/components/charts"; -import { MetricCard } from "@/components/metric-card"; -import { LineChart, type LineChartSeries } from "@/components/charts/LineChart"; -import { getMetricsHistory, type MetricsHistoryRow } from "../host-metrics-api"; +import { type MetricsHistoryRow, useHostMetricsApi } from "../host-metrics-api"; import { CardTimeTabs, type HistoryTab } from "./CardTimeTabs"; export function DiskCard({ @@ -18,6 +23,7 @@ export function DiskCard({ history: number[]; hostId: number | null; }) { + const { getMetricsHistory } = useHostMetricsApi(); const { t } = useTranslation(); const [activeTab, setActiveTab] = useState("live"); const [rows, setRows] = useState(null); diff --git a/plugins/host-metrics/src/frontend/cards/FilesystemPicker.tsx b/plugins/host-metrics/src/frontend/cards/FilesystemPicker.tsx index 243b43cb9..70a2eeead 100644 --- a/plugins/host-metrics/src/frontend/cards/FilesystemPicker.tsx +++ b/plugins/host-metrics/src/frontend/cards/FilesystemPicker.tsx @@ -1,13 +1,13 @@ -import { ChevronDown, Check } from "lucide-react"; -import { useTranslation } from "@termix/plugin-sdk/frontend"; -import type { DiskFilesystem } from "@/main-axios"; -import { cn } from "@/lib/utils"; import { + cn, DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, -} from "@/components/dropdown-menu"; +} from "@termix/plugin-sdk/ui"; +import { type DiskFilesystem } from "../../shared/metrics.js"; +import { ChevronDown, Check } from "lucide-react"; +import { useTranslation } from "@termix/plugin-sdk/frontend"; export function FilesystemPicker({ filesystems, diff --git a/plugins/host-metrics/src/frontend/cards/FirewallCard.tsx b/plugins/host-metrics/src/frontend/cards/FirewallCard.tsx index 337fd942d..75a87faf2 100644 --- a/plugins/host-metrics/src/frontend/cards/FirewallCard.tsx +++ b/plugins/host-metrics/src/frontend/cards/FirewallCard.tsx @@ -1,7 +1,7 @@ +import { MetricCard } from "@termix/plugin-sdk/ui"; +import { type ServerMetrics } from "../../shared/metrics.js"; import { ShieldCheck } from "lucide-react"; import { useTranslation } from "@termix/plugin-sdk/frontend"; -import type { ServerMetrics } from "@/main-axios"; -import { MetricCard } from "@/components/metric-card"; export function FirewallCard({ metrics }: { metrics: ServerMetrics | null }) { const { t } = useTranslation(); diff --git a/plugins/host-metrics/src/frontend/cards/GpuCard.tsx b/plugins/host-metrics/src/frontend/cards/GpuCard.tsx index 97192aa1c..4044b1091 100644 --- a/plugins/host-metrics/src/frontend/cards/GpuCard.tsx +++ b/plugins/host-metrics/src/frontend/cards/GpuCard.tsx @@ -1,10 +1,15 @@ +import { + MiniStat, + RadialGauge, + Sparkline, + StatRow, + MetricCard, +} from "@termix/plugin-sdk/ui"; +import { type ServerMetrics } from "../../shared/metrics.js"; +import { type GpuDevice, type GpuProcess } from "../../shared/stats-widgets.js"; import { useId } from "react"; import { Gpu } from "lucide-react"; import { useTranslation } from "@termix/plugin-sdk/frontend"; -import type { ServerMetrics } from "@/main-axios"; -import type { GpuDevice, GpuProcess } from "@/types/stats-widgets"; -import { MiniStat, RadialGauge, Sparkline, StatRow } from "@/components/charts"; -import { MetricCard } from "@/components/metric-card"; import type { GpuHistories, GpuHistory } from "./gpu-history"; const NA = "N/A"; diff --git a/plugins/host-metrics/src/frontend/cards/LoginStatsCard.tsx b/plugins/host-metrics/src/frontend/cards/LoginStatsCard.tsx index a221d6686..5e0017eec 100644 --- a/plugins/host-metrics/src/frontend/cards/LoginStatsCard.tsx +++ b/plugins/host-metrics/src/frontend/cards/LoginStatsCard.tsx @@ -1,7 +1,7 @@ +import { MetricCard } from "@termix/plugin-sdk/ui"; +import { type ServerMetrics } from "../../shared/metrics.js"; import { UserCheck, UserX } from "lucide-react"; import { useTranslation } from "@termix/plugin-sdk/frontend"; -import type { ServerMetrics } from "@/main-axios"; -import { MetricCard } from "@/components/metric-card"; export function LoginStatsCard({ metrics }: { metrics: ServerMetrics | null }) { const { t } = useTranslation(); diff --git a/plugins/host-metrics/src/frontend/cards/MemoryCard.tsx b/plugins/host-metrics/src/frontend/cards/MemoryCard.tsx index 65d9766e4..18386e3f2 100644 --- a/plugins/host-metrics/src/frontend/cards/MemoryCard.tsx +++ b/plugins/host-metrics/src/frontend/cards/MemoryCard.tsx @@ -1,11 +1,16 @@ +import { + RadialGauge, + Sparkline, + MiniStat, + MetricCard, + LineChart, + type LineChartSeries, +} from "@termix/plugin-sdk/ui"; +import { type ServerMetrics } from "../../shared/metrics.js"; import { useState, useEffect } from "react"; import { MemoryStick } from "lucide-react"; import { useTranslation } from "@termix/plugin-sdk/frontend"; -import type { ServerMetrics } from "@/main-axios"; -import { RadialGauge, Sparkline, MiniStat } from "@/components/charts"; -import { MetricCard } from "@/components/metric-card"; -import { LineChart, type LineChartSeries } from "@/components/charts/LineChart"; -import { getMetricsHistory, type MetricsHistoryRow } from "../host-metrics-api"; +import { type MetricsHistoryRow, useHostMetricsApi } from "../host-metrics-api"; import { CardTimeTabs, type HistoryTab } from "./CardTimeTabs"; export function MemoryCard({ @@ -17,6 +22,7 @@ export function MemoryCard({ history: number[]; hostId: number | null; }) { + const { getMetricsHistory } = useHostMetricsApi(); const { t } = useTranslation(); const [activeTab, setActiveTab] = useState("live"); const [rows, setRows] = useState(null); diff --git a/plugins/host-metrics/src/frontend/cards/NetworkCard.tsx b/plugins/host-metrics/src/frontend/cards/NetworkCard.tsx index cd9696395..3c0c2bdf2 100644 --- a/plugins/host-metrics/src/frontend/cards/NetworkCard.tsx +++ b/plugins/host-metrics/src/frontend/cards/NetworkCard.tsx @@ -1,10 +1,13 @@ +import { + MetricCard, + LineChart, + type LineChartSeries, +} from "@termix/plugin-sdk/ui"; +import { type ServerMetrics } from "../../shared/metrics.js"; import { useState, useEffect } from "react"; import { Cable, Container, Network, Wifi, WifiOff } from "lucide-react"; import { useTranslation } from "@termix/plugin-sdk/frontend"; -import type { ServerMetrics } from "@/main-axios"; -import { MetricCard } from "@/components/metric-card"; -import { LineChart, type LineChartSeries } from "@/components/charts/LineChart"; -import { getMetricsHistory, type MetricsHistoryRow } from "../host-metrics-api"; +import { type MetricsHistoryRow, useHostMetricsApi } from "../host-metrics-api"; import { CardTimeTabs, type HistoryTab } from "./CardTimeTabs"; function ifaceIcon(name: string) { @@ -65,6 +68,7 @@ export function NetworkCard({ metrics: ServerMetrics | null; hostId: number | null; }) { + const { getMetricsHistory } = useHostMetricsApi(); const { t } = useTranslation(); const [activeTab, setActiveTab] = useState("live"); const [rows, setRows] = useState(null); diff --git a/plugins/host-metrics/src/frontend/cards/PortsCard.tsx b/plugins/host-metrics/src/frontend/cards/PortsCard.tsx index d898ffe92..ded9f962a 100644 --- a/plugins/host-metrics/src/frontend/cards/PortsCard.tsx +++ b/plugins/host-metrics/src/frontend/cards/PortsCard.tsx @@ -1,9 +1,11 @@ +import { MetricCard, Select2 } from "@termix/plugin-sdk/ui"; +import { + type ServerMetrics, + type ListeningPort, +} from "../../shared/metrics.js"; import { useMemo, useState } from "react"; import { Unplug, Search } from "lucide-react"; import { useTranslation } from "@termix/plugin-sdk/frontend"; -import type { ServerMetrics, ListeningPort } from "@/main-axios"; -import { MetricCard } from "@/components/metric-card"; -import { Select2 } from "@/components/select2"; function formatAddress(addr: string) { return addr === "0.0.0.0" || addr === "*" || addr === "::" ? "*" : addr; diff --git a/plugins/host-metrics/src/frontend/cards/ProcessesCard.tsx b/plugins/host-metrics/src/frontend/cards/ProcessesCard.tsx index 51b93e889..27a91bc5c 100644 --- a/plugins/host-metrics/src/frontend/cards/ProcessesCard.tsx +++ b/plugins/host-metrics/src/frontend/cards/ProcessesCard.tsx @@ -1,8 +1,7 @@ +import { StatRow, MetricCard } from "@termix/plugin-sdk/ui"; +import { type ServerMetrics } from "../../shared/metrics.js"; import { Cpu } from "lucide-react"; import { useTranslation } from "@termix/plugin-sdk/frontend"; -import type { ServerMetrics } from "@/main-axios"; -import { StatRow } from "@/components/charts"; -import { MetricCard } from "@/components/metric-card"; export function ProcessesCard({ metrics }: { metrics: ServerMetrics | null }) { const { t } = useTranslation(); diff --git a/plugins/host-metrics/src/frontend/cards/SystemCard.tsx b/plugins/host-metrics/src/frontend/cards/SystemCard.tsx index 78e452183..fd87b7a8c 100644 --- a/plugins/host-metrics/src/frontend/cards/SystemCard.tsx +++ b/plugins/host-metrics/src/frontend/cards/SystemCard.tsx @@ -1,8 +1,7 @@ +import { StatRow, MetricCard } from "@termix/plugin-sdk/ui"; +import { type ServerMetrics } from "../../shared/metrics.js"; import { Server } from "lucide-react"; import { useTranslation } from "@termix/plugin-sdk/frontend"; -import type { ServerMetrics } from "@/main-axios"; -import { StatRow } from "@/components/charts"; -import { MetricCard } from "@/components/metric-card"; export function SystemCard({ metrics }: { metrics: ServerMetrics | null }) { const { t } = useTranslation(); diff --git a/plugins/host-metrics/src/frontend/cards/TemperatureCard.tsx b/plugins/host-metrics/src/frontend/cards/TemperatureCard.tsx index dce1d9384..ef0b4ec40 100644 --- a/plugins/host-metrics/src/frontend/cards/TemperatureCard.tsx +++ b/plugins/host-metrics/src/frontend/cards/TemperatureCard.tsx @@ -1,20 +1,15 @@ +import { StatRow, MetricCard } from "@termix/plugin-sdk/ui"; +import { type ServerMetrics } from "../../shared/metrics.js"; import { Thermometer } from "lucide-react"; -import { useTranslation } from "@termix/plugin-sdk/frontend"; -import type { ServerMetrics } from "@/main-axios"; -import { StatRow } from "@/components/charts"; -import { MetricCard } from "@/components/metric-card"; +import { useSettings, useTranslation } from "@termix/plugin-sdk/frontend"; import { useEffect, useState } from "react"; import { + formatTemperature, selectTemperatureSensor, temperaturePreferenceKey, + temperatureUnitOf, } from "../temperature-preference"; -function formatTemperature(value: number | null | undefined): string { - return typeof value === "number" && Number.isFinite(value) - ? `${value.toFixed(1)}°C` - : "N/A"; -} - export function TemperatureCard({ metrics, hostId, @@ -23,6 +18,7 @@ export function TemperatureCard({ hostId: number | null; }) { const { t } = useTranslation(); + const unit = temperatureUnitOf(useSettings("user").values.temperatureUnit); const temperature = metrics?.temperature; const sensors = temperature?.sensors ?? []; const [preferredLabel, setPreferredLabel] = useState(""); @@ -59,7 +55,7 @@ export function TemperatureCard({
- {formatTemperature(displayedTemperature)} + {formatTemperature(displayedTemperature, unit)}
{displayedLabel} @@ -92,7 +88,7 @@ export function TemperatureCard({ ))} diff --git a/plugins/host-metrics/src/frontend/cards/UptimeCard.tsx b/plugins/host-metrics/src/frontend/cards/UptimeCard.tsx index d3c3dea4e..467159452 100644 --- a/plugins/host-metrics/src/frontend/cards/UptimeCard.tsx +++ b/plugins/host-metrics/src/frontend/cards/UptimeCard.tsx @@ -1,7 +1,7 @@ +import { MetricCard } from "@termix/plugin-sdk/ui"; +import { type ServerMetrics } from "../../shared/metrics.js"; import { Clock } from "lucide-react"; import { useTranslation } from "@termix/plugin-sdk/frontend"; -import type { ServerMetrics } from "@/main-axios"; -import { MetricCard } from "@/components/metric-card"; export function UptimeCard({ metrics }: { metrics: ServerMetrics | null }) { const { t } = useTranslation(); diff --git a/plugins/host-metrics/src/frontend/cards/gpu-history.ts b/plugins/host-metrics/src/frontend/cards/gpu-history.ts index b8fc7554d..970241bda 100644 --- a/plugins/host-metrics/src/frontend/cards/gpu-history.ts +++ b/plugins/host-metrics/src/frontend/cards/gpu-history.ts @@ -1,5 +1,3 @@ -import type { GpuDevice } from "@/types/stats-widgets"; - export interface GpuHistory { utilization: number[]; memory: number[]; @@ -29,3 +27,4 @@ export function appendGpuHistories( } return next; } +import { type GpuDevice } from "../../shared/stats-widgets.js"; diff --git a/plugins/host-metrics/src/frontend/cards/index.tsx b/plugins/host-metrics/src/frontend/cards/index.tsx index 4b4c3aef7..6bc7ca9f4 100644 --- a/plugins/host-metrics/src/frontend/cards/index.tsx +++ b/plugins/host-metrics/src/frontend/cards/index.tsx @@ -1,10 +1,10 @@ -import type { ComponentType, ReactNode } from "react"; -import type { ServerMetrics } from "@/main-axios"; +import { type ServerMetrics } from "../../shared/metrics.js"; import { defaultColSpanFor, defaultHeightFor, type HostMetricsCardId, -} from "@/types/host-metrics"; +} from "../../shared/host-metrics.js"; +import type { ComponentType, ReactNode } from "react"; import { CpuCard } from "./CpuCard"; import { MemoryCard } from "./MemoryCard"; import { DiskCard } from "./DiskCard"; diff --git a/plugins/host-metrics/src/frontend/cards/managers/CronManagerCard.tsx b/plugins/host-metrics/src/frontend/cards/managers/CronManagerCard.tsx index 87a22f79a..c1be0b4ee 100644 --- a/plugins/host-metrics/src/frontend/cards/managers/CronManagerCard.tsx +++ b/plugins/host-metrics/src/frontend/cards/managers/CronManagerCard.tsx @@ -1,9 +1,9 @@ +import { Button } from "@termix/plugin-sdk/ui"; import { useEffect, useMemo, useState } from "react"; import { Clock4, Plus, Trash2, Save } from "lucide-react"; import { useTranslation } from "@termix/plugin-sdk/frontend"; import { toast } from "sonner"; -import { Button } from "@/components/button"; -import { managerPost } from "../../host-metrics-api"; +import { useHostMetricsApi } from "../../host-metrics-api"; import { useManagerData, extractError } from "./useManagerData"; import { ManagerCardShell } from "./ManagerCardShell"; import { ManagerSearch } from "./ManagerToolbar"; @@ -16,6 +16,7 @@ interface CronEntry { } export function CronManagerCard({ hostId }: { hostId: number | null }) { + const { managerPost } = useHostMetricsApi(); const { t } = useTranslation(); const { data, loading, error, refresh } = useManagerData<{ entries: CronEntry[]; diff --git a/plugins/host-metrics/src/frontend/cards/managers/FirewallManagerCard.tsx b/plugins/host-metrics/src/frontend/cards/managers/FirewallManagerCard.tsx index 351f66b1c..41722de5c 100644 --- a/plugins/host-metrics/src/frontend/cards/managers/FirewallManagerCard.tsx +++ b/plugins/host-metrics/src/frontend/cards/managers/FirewallManagerCard.tsx @@ -1,14 +1,13 @@ +import { Button, Select2 } from "@termix/plugin-sdk/ui"; +import { type FirewallChain } from "../../../shared/metrics.js"; import { useMemo, useState } from "react"; import { ShieldCheck, Plus, Trash2, Save } from "lucide-react"; import { useTranslation } from "@termix/plugin-sdk/frontend"; import { toast } from "sonner"; -import { Button } from "@/components/button"; -import { type FirewallChain } from "@/main-axios"; -import { managerPost } from "../../host-metrics-api"; +import { useHostMetricsApi } from "../../host-metrics-api"; import { useManagerData, extractError } from "./useManagerData"; import { ManagerCardShell } from "./ManagerCardShell"; import { ManagerSearch } from "./ManagerToolbar"; -import { Select2 } from "@/components/select2"; interface FirewallData { type: "iptables" | "nftables" | "none"; @@ -17,6 +16,7 @@ interface FirewallData { } export function FirewallManagerCard({ hostId }: { hostId: number | null }) { + const { managerPost } = useHostMetricsApi(); const { t } = useTranslation(); const { data, loading, error, refresh } = useManagerData( hostId, diff --git a/plugins/host-metrics/src/frontend/cards/managers/HealthCheckCard.tsx b/plugins/host-metrics/src/frontend/cards/managers/HealthCheckCard.tsx index 0c5add399..de3cbef9d 100644 --- a/plugins/host-metrics/src/frontend/cards/managers/HealthCheckCard.tsx +++ b/plugins/host-metrics/src/frontend/cards/managers/HealthCheckCard.tsx @@ -1,3 +1,4 @@ +import { Button, Sparkline, Select2 } from "@termix/plugin-sdk/ui"; import { useEffect, useMemo, useState } from "react"; import { HeartPulse, @@ -10,12 +11,9 @@ import { } from "lucide-react"; import { useTranslation } from "@termix/plugin-sdk/frontend"; import { toast } from "sonner"; -import { Button } from "@/components/button"; -import { Sparkline } from "@/components/charts"; -import { managerPost } from "../../host-metrics-api"; +import { useHostMetricsApi } from "../../host-metrics-api"; import { useManagerData, extractError } from "./useManagerData"; import { ManagerCardShell } from "./ManagerCardShell"; -import { Select2 } from "@/components/select2"; interface HealthCheck { id: string; @@ -54,6 +52,7 @@ function newCheck(): HealthCheck { } export function HealthCheckCard({ hostId }: { hostId: number | null }) { + const { managerPost } = useHostMetricsApi(); const { t } = useTranslation(); const { data, loading, error, refresh } = useManagerData( hostId, diff --git a/plugins/host-metrics/src/frontend/cards/managers/LogViewerCard.tsx b/plugins/host-metrics/src/frontend/cards/managers/LogViewerCard.tsx index e175c5d18..89e71fbab 100644 --- a/plugins/host-metrics/src/frontend/cards/managers/LogViewerCard.tsx +++ b/plugins/host-metrics/src/frontend/cards/managers/LogViewerCard.tsx @@ -1,11 +1,10 @@ +import { useAdaptivePolling, Select2 } from "@termix/plugin-sdk/ui"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { ScrollText } from "lucide-react"; import { useTranslation } from "@termix/plugin-sdk/frontend"; -import { managerGet, managerGetSub } from "../../host-metrics-api"; +import { useHostMetricsApi } from "../../host-metrics-api"; import { extractError } from "./useManagerData"; import { ManagerCardShell } from "./ManagerCardShell"; -import { useAdaptivePolling } from "@/hooks/use-adaptive-polling"; -import { Select2 } from "@/components/select2"; interface LogFiles { common: string[]; @@ -15,6 +14,7 @@ interface LogFiles { type Mode = "file" | "unit"; export function LogViewerCard({ hostId }: { hostId: number | null }) { + const { managerGet, managerGetSub } = useHostMetricsApi(); const { t } = useTranslation(); const [mode, setMode] = useState("file"); const [path, setPath] = useState("/var/log/syslog"); diff --git a/plugins/host-metrics/src/frontend/cards/managers/ManagerCardShell.tsx b/plugins/host-metrics/src/frontend/cards/managers/ManagerCardShell.tsx index a55e1ef36..5037131d3 100644 --- a/plugins/host-metrics/src/frontend/cards/managers/ManagerCardShell.tsx +++ b/plugins/host-metrics/src/frontend/cards/managers/ManagerCardShell.tsx @@ -1,8 +1,7 @@ +import { Button, MetricCard } from "@termix/plugin-sdk/ui"; import type { ReactNode } from "react"; import { RefreshCw, AlertTriangle } from "lucide-react"; import { useTranslation } from "@termix/plugin-sdk/frontend"; -import { Button } from "@/components/button"; -import { MetricCard } from "@/components/metric-card"; import type { ManagerError } from "./useManagerData"; export function ManagerCardShell({ diff --git a/plugins/host-metrics/src/frontend/cards/managers/PackageManagerCard.tsx b/plugins/host-metrics/src/frontend/cards/managers/PackageManagerCard.tsx index 7b9d77a66..f75216460 100644 --- a/plugins/host-metrics/src/frontend/cards/managers/PackageManagerCard.tsx +++ b/plugins/host-metrics/src/frontend/cards/managers/PackageManagerCard.tsx @@ -1,9 +1,9 @@ +import { Button } from "@termix/plugin-sdk/ui"; import { useMemo, useState } from "react"; import { Package, ArrowUpCircle } from "lucide-react"; import { useTranslation } from "@termix/plugin-sdk/frontend"; import { toast } from "sonner"; -import { Button } from "@/components/button"; -import { managerPost } from "../../host-metrics-api"; +import { useHostMetricsApi } from "../../host-metrics-api"; import { useManagerData, extractError } from "./useManagerData"; import { ManagerCardShell } from "./ManagerCardShell"; import { ManagerSearch } from "./ManagerToolbar"; @@ -15,6 +15,7 @@ interface UpgradablePackage { } export function PackageManagerCard({ hostId }: { hostId: number | null }) { + const { managerPost } = useHostMetricsApi(); const { t } = useTranslation(); const { data, loading, error, refresh } = useManagerData<{ pkg: string | null; diff --git a/plugins/host-metrics/src/frontend/cards/managers/ProcessInspectorCard.tsx b/plugins/host-metrics/src/frontend/cards/managers/ProcessInspectorCard.tsx index d828bb922..e38f89afb 100644 --- a/plugins/host-metrics/src/frontend/cards/managers/ProcessInspectorCard.tsx +++ b/plugins/host-metrics/src/frontend/cards/managers/ProcessInspectorCard.tsx @@ -1,12 +1,12 @@ +import { Select2 } from "@termix/plugin-sdk/ui"; import { useMemo, useState } from "react"; import { ListTree, X, Skull } from "lucide-react"; import { useTranslation } from "@termix/plugin-sdk/frontend"; import { toast } from "sonner"; -import { managerPost } from "../../host-metrics-api"; +import { useHostMetricsApi } from "../../host-metrics-api"; import { useManagerData, extractError } from "./useManagerData"; import { ManagerCardShell } from "./ManagerCardShell"; import { ManagerSearch } from "./ManagerToolbar"; -import { Select2 } from "@/components/select2"; interface ProcessRow { pid: number; @@ -22,6 +22,7 @@ type SortKey = "cpu" | "mem" | "pid"; type Signal = "TERM" | "KILL"; export function ProcessInspectorCard({ hostId }: { hostId: number | null }) { + const { managerPost } = useHostMetricsApi(); const { t } = useTranslation(); const { data, loading, error, refresh } = useManagerData<{ processes: ProcessRow[]; diff --git a/plugins/host-metrics/src/frontend/cards/managers/ServiceManagerCard.tsx b/plugins/host-metrics/src/frontend/cards/managers/ServiceManagerCard.tsx index 1442adc9c..7add9fcd2 100644 --- a/plugins/host-metrics/src/frontend/cards/managers/ServiceManagerCard.tsx +++ b/plugins/host-metrics/src/frontend/cards/managers/ServiceManagerCard.tsx @@ -2,7 +2,7 @@ import { useMemo, useState } from "react"; import { Cog, Play, Square, RotateCw, Power } from "lucide-react"; import { useTranslation } from "@termix/plugin-sdk/frontend"; import { toast } from "sonner"; -import { managerPost } from "../../host-metrics-api"; +import { useHostMetricsApi } from "../../host-metrics-api"; import { useManagerData, extractError } from "./useManagerData"; import { ManagerCardShell } from "./ManagerCardShell"; import { ManagerSearch } from "./ManagerToolbar"; @@ -15,6 +15,7 @@ interface SystemdService { } export function ServiceManagerCard({ hostId }: { hostId: number | null }) { + const { managerPost } = useHostMetricsApi(); const { t } = useTranslation(); const { data, loading, error, refresh } = useManagerData<{ services: SystemdService[]; diff --git a/plugins/host-metrics/src/frontend/cards/managers/SimpleManagerCards.tsx b/plugins/host-metrics/src/frontend/cards/managers/SimpleManagerCards.tsx index 2cfa220fc..2a675e5b3 100644 --- a/plugins/host-metrics/src/frontend/cards/managers/SimpleManagerCards.tsx +++ b/plugins/host-metrics/src/frontend/cards/managers/SimpleManagerCards.tsx @@ -1,7 +1,7 @@ +import { BarSeries, StatRow } from "@termix/plugin-sdk/ui"; import { useMemo, useState } from "react"; import { MemoryStick, Timer, HardDrive } from "lucide-react"; import { useTranslation } from "@termix/plugin-sdk/frontend"; -import { BarSeries, StatRow } from "@/components/charts"; import { useManagerData } from "./useManagerData"; import { ManagerCardShell } from "./ManagerCardShell"; import { ManagerSearch } from "./ManagerToolbar"; diff --git a/plugins/host-metrics/src/frontend/cards/managers/SslManagerCard.tsx b/plugins/host-metrics/src/frontend/cards/managers/SslManagerCard.tsx index db632155e..b06ddc03c 100644 --- a/plugins/host-metrics/src/frontend/cards/managers/SslManagerCard.tsx +++ b/plugins/host-metrics/src/frontend/cards/managers/SslManagerCard.tsx @@ -1,15 +1,14 @@ +import { Button, Select2 } from "@termix/plugin-sdk/ui"; import { useMemo, useState } from "react"; import { ShieldCheck, RefreshCw, Plus, X, Trash2 } from "lucide-react"; import { useTranslation } from "@termix/plugin-sdk/frontend"; -import { Button } from "@/components/button"; -import { managerPost } from "../../host-metrics-api"; +import { useHostMetricsApi } from "../../host-metrics-api"; import { useManagerData, useManagerAction, extractError, } from "./useManagerData"; import { ManagerCardShell } from "./ManagerCardShell"; -import { Select2 } from "@/components/select2"; interface CertInfo { client: string; @@ -53,6 +52,7 @@ function ExpiryBadge({ expiry }: { expiry: string | null }) { } export function SslManagerCard({ hostId }: { hostId: number | null }) { + const { managerPost } = useHostMetricsApi(); const { t } = useTranslation(); const { data, loading, error, refresh } = useManagerData( hostId, diff --git a/plugins/host-metrics/src/frontend/cards/managers/UserManagerCard.tsx b/plugins/host-metrics/src/frontend/cards/managers/UserManagerCard.tsx index f1f577056..6ef343b25 100644 --- a/plugins/host-metrics/src/frontend/cards/managers/UserManagerCard.tsx +++ b/plugins/host-metrics/src/frontend/cards/managers/UserManagerCard.tsx @@ -1,9 +1,9 @@ +import { Button } from "@termix/plugin-sdk/ui"; import { useMemo, useState } from "react"; import { Users, Plus, Trash2, ShieldCheck, Shield } from "lucide-react"; import { useTranslation } from "@termix/plugin-sdk/frontend"; import { toast } from "sonner"; -import { Button } from "@/components/button"; -import { managerPost } from "../../host-metrics-api"; +import { useHostMetricsApi } from "../../host-metrics-api"; import { useManagerData, extractError } from "./useManagerData"; import { ManagerCardShell } from "./ManagerCardShell"; import { ManagerSearch } from "./ManagerToolbar"; @@ -19,6 +19,7 @@ interface UsersData { } export function UserManagerCard({ hostId }: { hostId: number | null }) { + const { managerPost } = useHostMetricsApi(); const { t } = useTranslation(); const { data, loading, error, refresh } = useManagerData( hostId, diff --git a/plugins/host-metrics/src/frontend/cards/managers/useManagerData.ts b/plugins/host-metrics/src/frontend/cards/managers/useManagerData.ts index 56bc03057..6e2de7b87 100644 --- a/plugins/host-metrics/src/frontend/cards/managers/useManagerData.ts +++ b/plugins/host-metrics/src/frontend/cards/managers/useManagerData.ts @@ -1,6 +1,6 @@ import { useCallback, useEffect, useState } from "react"; import { toast } from "sonner"; -import { managerGet, managerPost } from "../../host-metrics-api"; +import { useHostMetricsApi } from "../../host-metrics-api"; interface ManagerError { message: string; @@ -27,6 +27,7 @@ export function useManagerData( resource: string, params?: Record, ) { + const { managerGet } = useHostMetricsApi(); const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); @@ -66,6 +67,7 @@ interface ActionResult { * to refresh the card). Returns the result so callers can branch further. */ export function useManagerAction(hostId: number | null) { + const { managerPost } = useHostMetricsApi(); const [busy, setBusy] = useState(false); const run = useCallback( diff --git a/plugins/host-metrics/src/frontend/hooks/useHostMetricsPreferences.ts b/plugins/host-metrics/src/frontend/hooks/useHostMetricsPreferences.ts index e42f99984..08baa2490 100644 --- a/plugins/host-metrics/src/frontend/hooks/useHostMetricsPreferences.ts +++ b/plugins/host-metrics/src/frontend/hooks/useHostMetricsPreferences.ts @@ -1,9 +1,6 @@ +import { type HostMetricsLayout } from "../../shared/host-metrics.js"; import { useCallback, useEffect, useRef, useState } from "react"; -import { - getHostMetricsLayout, - saveHostMetricsLayout, -} from "../host-metrics-api"; -import type { HostMetricsLayout } from "@/types/host-metrics"; +import { useHostMetricsApi } from "../host-metrics-api"; const SAVE_DEBOUNCE_MS = 800; @@ -38,6 +35,7 @@ function writeCache(hostId: number, layout: HostMetricsLayout) { * debounced. Returns null layout until the first load resolves. */ export function useHostMetricsPreferences(hostId: number | null) { + const { getHostMetricsLayout, saveHostMetricsLayout } = useHostMetricsApi(); const [layout, setLayout] = useState(() => hostId != null ? readCache(hostId) : null, ); diff --git a/plugins/host-metrics/src/frontend/host-metrics-api.ts b/plugins/host-metrics/src/frontend/host-metrics-api.ts index f7e06ee33..ad216c65e 100644 --- a/plugins/host-metrics/src/frontend/host-metrics-api.ts +++ b/plugins/host-metrics/src/frontend/host-metrics-api.ts @@ -1,14 +1,14 @@ -import { handleApiError, statsApi } from "@/main-axios"; -import type { HostMetricsLayout } from "@/types/host-metrics"; +import { useMemo } from "react"; +import { + usePluginApi, + type PluginApiClient, +} from "@termix/plugin-sdk/frontend"; +import type { HostMetricsLayout } from "../shared/host-metrics.js"; +import type { ServerMetrics } from "../shared/metrics.js"; -// Every function below is keyed by a host's numeric database id, and the -// receiving backend must own that host in its own database -- a synced -// host has a different numeric id on each side (only its syncId matches -// across them). These calls always target the embedded local backend; see -// getAllServerStatuses in host-metrics-status-api.ts for the one metrics -// call that IS safely merged across local + remote (a process-local, -// in-memory aggregate keyed by whichever host ids that process happens to -// know about, not a per-host lookup). +// Every call is keyed by a host's numeric database id, and the receiving +// backend must own that host in its own database: a synced host has a +// different numeric id on each side. They always target this backend. export interface MetricsHistoryRow { ts: string; @@ -25,63 +25,6 @@ export interface MetricsHistoryResponse { toTs: string; } -export async function getMetricsHistory( - hostId: number, - opts: { range?: string; from?: string; to?: string }, -): Promise { - const res = await statsApi.get(`/metrics/history/${hostId}`, { - params: opts, - }); - return res.data as MetricsHistoryResponse; -} - -export async function getMetricsHistoryRetention(): Promise { - const res = await statsApi.get("/global-settings/history"); - return (res.data as { metricsHistoryRetentionDays: number }) - .metricsHistoryRetentionDays; -} - -export async function saveMetricsHistoryRetention(days: number): Promise { - await statsApi.post("/global-settings/history", { - metricsHistoryRetentionDays: days, - }); -} - -/** - * Host Metrics layout persistence (server-synced per user/host) + the manager - * card endpoints. All routes live under the `/host-metrics/*` prefix on the - * stats app (port 30005). - */ - -export async function getHostMetricsLayout( - hostId: number, -): Promise { - try { - const res = await statsApi.get(`/host-metrics/preferences/${hostId}`, { - validateStatus: (status) => status === 200 || status === 404, - }); - if (res.status === 404) return null; - return res.data?.layout ?? null; - } catch (error) { - handleApiError(error, "fetch host metrics layout"); - throw error; - } -} - -export async function saveHostMetricsLayout( - hostId: number, - layout: HostMetricsLayout, -): Promise { - try { - await statsApi.post(`/host-metrics/preferences/${hostId}`, layout); - } catch (error) { - handleApiError(error, "save host metrics layout"); - throw error; - } -} - -// ─── Managers ─────────────────────────────────────────────────────────────── - export interface PlatformInfo { hasSystemd: boolean; pkg: "apt" | "dnf" | "yum" | "pacman" | null; @@ -91,56 +34,193 @@ export interface PlatformInfo { osPrettyName: string | null; } -export async function getHostPlatform(hostId: number): Promise { - const res = await statsApi.get(`/host-metrics/platform/${hostId}`); - return res.data; +export interface ConnectionLogEntry { + type: "info" | "success" | "warning" | "error"; + stage: string; + message: string; + details?: Record; } -/** GET a manager resource (read). */ -export async function managerGet( - hostId: number, - resource: string, - params?: Record, -): Promise { - const res = await statsApi.get( - `/host-metrics/managers/${resource}/${hostId}`, - { params }, - ); - return res.data as T; +export interface StartMetricsResult { + success: boolean; + requires_totp?: boolean; + sessionId?: string; + prompt?: string; + viewerSessionId?: string; + connectionLogs?: ConnectionLogEntry[]; } -/** - * GET a manager sub-resource where the host id sits in the middle of the path, - * e.g. /managers/logs/{id}/files. - */ -export async function managerGetSub( - hostId: number, - resource: string, - sub: string, - params?: Record, -): Promise { - const res = await statsApi.get( - `/host-metrics/managers/${resource}/${hostId}/${sub}`, - { params }, - ); - return res.data as T; +function statusOf(error: unknown): number | undefined { + return (error as { response?: { status?: number } })?.response?.status; } -/** - * POST a manager action. `resource` is the manager name; `action` is the - * optional sub-path (e.g. "action", "signal", "renew"). The host id always - * sits between them: /managers/{resource}/{id}[/{action}]. - */ -export async function managerPost( - hostId: number, - resource: string, - body: unknown, - action?: string, -): Promise { - const suffix = action ? `/${action}` : ""; - const res = await statsApi.post( - `/host-metrics/managers/${resource}/${hostId}${suffix}`, - body, - ); - return res.data as T; +function bodyOf(error: unknown): Record | undefined { + return (error as { response?: { data?: Record } })?.response + ?.data; +} + +/** The plugin's routes, relative to /plugin-api/host-metrics/. */ +export function createHostMetricsApi(api: PluginApiClient) { + return { + async getMetrics(hostId: number): Promise { + try { + return (await api.get(`/metrics/${hostId}`)).data; + } catch (error) { + // No sample yet, or metrics are off for the host. + if (statusOf(error) === 404) return null; + throw error; + } + }, + + /** Throws an Error carrying the server's connectionLogs on failure. */ + async startMetrics(hostId: number): Promise { + try { + return (await api.post(`/metrics/start/${hostId}`)) + .data; + } catch (error) { + const body = bodyOf(error); + if (body?.connectionLogs) { + const wrapped = new Error( + String(body.error || body.message || "Failed to start metrics"), + ); + Object.assign(wrapped, { connectionLogs: body.connectionLogs }); + throw wrapped; + } + throw error; + } + }, + + async stopMetrics(hostId: number, viewerSessionId?: string) { + await api.post(`/metrics/stop/${hostId}`, { viewerSessionId }); + }, + + async submitTotp( + sessionId: string, + totpCode: string, + ): Promise<{ success: boolean; viewerSessionId?: string }> { + return ( + await api.post<{ success: boolean; viewerSessionId?: string }>( + "/metrics/connect-totp", + { sessionId, totpCode }, + ) + ).data; + }, + + /** False when the server no longer knows the viewer. */ + async heartbeat(viewerSessionId: string): Promise { + try { + await api.post("/metrics/heartbeat", { viewerSessionId }); + return true; + } catch (error) { + if (statusOf(error) === 404) return false; + throw error; + } + }, + + async registerViewer(hostId: number): Promise<{ + success: boolean; + viewerSessionId?: string; + skipped?: boolean; + reason?: string; + }> { + return (await api.post("/metrics/register-viewer", { hostId })).data as { + success: boolean; + viewerSessionId?: string; + skipped?: boolean; + reason?: string; + }; + }, + + async unregisterViewer(hostId: number, viewerSessionId: string) { + await api.post("/metrics/unregister-viewer", { + hostId, + viewerSessionId, + }); + }, + + async getMetricsHistory( + hostId: number, + opts: { range?: string; from?: string; to?: string }, + ): Promise { + return ( + await api.get(`/metrics/history/${hostId}`, { + params: opts, + }) + ).data; + }, + + async getHostMetricsLayout( + hostId: number, + ): Promise { + try { + const res = await api.get<{ layout?: HostMetricsLayout }>( + `/host-metrics/preferences/${hostId}`, + ); + return res.data?.layout ?? null; + } catch (error) { + if (statusOf(error) === 404) return null; + throw error; + } + }, + + async saveHostMetricsLayout(hostId: number, layout: HostMetricsLayout) { + await api.post(`/host-metrics/preferences/${hostId}`, layout); + }, + + async getHostPlatform(hostId: number): Promise { + return (await api.get(`/host-metrics/platform/${hostId}`)) + .data; + }, + + /** GET a manager resource (read). */ + async managerGet( + hostId: number, + resource: string, + params?: Record, + ): Promise { + return ( + await api.get(`/host-metrics/managers/${resource}/${hostId}`, { + params, + }) + ).data; + }, + + /** GET where the host id sits mid-path: /managers/logs/{id}/files. */ + async managerGetSub( + hostId: number, + resource: string, + sub: string, + params?: Record, + ): Promise { + return ( + await api.get( + `/host-metrics/managers/${resource}/${hostId}/${sub}`, + { params }, + ) + ).data; + }, + + /** POST a manager action: /managers/{resource}/{id}[/{action}]. */ + async managerPost( + hostId: number, + resource: string, + body: unknown, + action?: string, + ): Promise { + const suffix = action ? `/${action}` : ""; + return ( + await api.post( + `/host-metrics/managers/${resource}/${hostId}${suffix}`, + body, + ) + ).data; + }, + }; +} + +export type HostMetricsApi = ReturnType; + +export function useHostMetricsApi(): HostMetricsApi { + const api = usePluginApi(); + return useMemo(() => createHostMetricsApi(api), [api]); } diff --git a/plugins/host-metrics/src/frontend/index.tsx b/plugins/host-metrics/src/frontend/index.tsx index 4a3728237..e690d1a2c 100644 --- a/plugins/host-metrics/src/frontend/index.tsx +++ b/plugins/host-metrics/src/frontend/index.tsx @@ -3,24 +3,37 @@ import { Activity, Server } from "lucide-react"; import type { HomepageWidgetContribution, HostEditorSectionProps, + PluginHostRecord, StandaloneViewProps, TabProps, TermixApp, } from "@termix/plugin-sdk/frontend"; -import type { SSHHost } from "@/types"; import { HostMetricsTab } from "./HostMetricsTab"; import HostMetricsApp from "./HostMetricsApp"; import { HostStatsTab } from "./HostEditorStatsTab"; import { metricsChartWidget } from "./MetricsChartWidget"; -import { MetricsRetentionSetting } from "./MetricsRetentionSetting"; import { TerminalMetricsStatus } from "./TerminalMetricsStatus"; +import { createHostMetricsApi } from "./host-metrics-api"; +import { createMetricsSummaryStore } from "./summary-store"; +import { + createDashboardHostMetrics, + createHomepageHostMetrics, +} from "./SummaryViews"; -type SectionSetField = Parameters[0]["setField"]; +type HostMetricsTabConfig = Parameters[0]["hostConfig"]; + +/** This plugin's metricsEnabled host setting; on unless turned off. */ +function metricsEnabledFor(host: unknown): boolean { + const bag = ( + host as { pluginSettings?: Record> } + )?.pluginSettings; + return bag?.["host-metrics"]?.metricsEnabled !== false; +} function MetricsTab({ sshHost, label, isVisible }: TabProps) { return ( ; } -function MetricsHostSection({ - form, - setField, - snippets, -}: HostEditorSectionProps) { - return ( - - ); +function MetricsHostSection(props: HostEditorSectionProps) { + return ; } -export function activate(app: TermixApp): void { +export async function activate(app: TermixApp): Promise { + const allowed = await app.hasPermission("use"); + const metricsApi = createHostMetricsApi(app.api); + const summaries = createMetricsSummaryStore(metricsApi); + app.onDispose(() => summaries.dispose()); + + // The latest disk sample, for the file manager's usage bar. + app.registerAction( + "host-metrics.disk", + (async (hostId: number) => + (await metricsApi.getMetrics(hostId).catch(() => null))?.disk ?? + null) as never, + { permission: "use" }, + ); + app.registerTab("host-metrics", MetricsTab, { icon: Server, titleKey: "nav.hostMetrics", @@ -70,10 +87,8 @@ export function activate(app: TermixApp): void { tabType: "host-metrics", copyUrlView: "host-metrics", overview: true, - when: (host) => - !!host.enableSsh && - (host.statsConfig as { metricsEnabled?: boolean } | undefined) - ?.metricsEnabled !== false, + when: (host: PluginHostRecord) => + allowed && !!host.enableSsh && metricsEnabledFor(host), }); app.registerHostEditorSection({ @@ -89,17 +104,29 @@ export function activate(app: TermixApp): void { metricsChartWidget as unknown as HomepageWidgetContribution, ); - 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, + when: (context) => allowed && metricsEnabledFor(context.host), + }); + + // The usage bars in the dashboard's host status card. + app.registerSlotContribution("dashboard.hostMetrics", { + actionId: "host-metrics.dashboardHost", + titleKey: "nav.hostMetrics", + kind: "component", + component: createDashboardHostMetrics(summaries), + }); + + // The numbers in the homepage's host status widget. + app.registerSlotContribution("homepage.hostMetrics", { + actionId: "host-metrics.homepageHost", + titleKey: "nav.hostMetrics", + kind: "component", + component: createHomepageHostMetrics(summaries), }); app.registerSlotContribution("onboarding.features", { diff --git a/plugins/host-metrics/src/frontend/metrics-change-key.ts b/plugins/host-metrics/src/frontend/metrics-change-key.ts index c41071e61..75ae09d75 100644 --- a/plugins/host-metrics/src/frontend/metrics-change-key.ts +++ b/plugins/host-metrics/src/frontend/metrics-change-key.ts @@ -1,5 +1,3 @@ -import type { ServerMetrics } from "@/main-axios"; - /** * Coarse fingerprint of a metrics poll. Adaptive polling backs off while it * stays the same, so it must move whenever something a card shows changes. @@ -22,3 +20,4 @@ export function metricsChangeKey(data: ServerMetrics): string { gpuProcesses: data.gpu?.processes?.length ?? 0, }); } +import { type ServerMetrics } from "../shared/metrics.js"; diff --git a/plugins/host-metrics/src/frontend/summary-store.ts b/plugins/host-metrics/src/frontend/summary-store.ts new file mode 100644 index 000000000..d9217ba1a --- /dev/null +++ b/plugins/host-metrics/src/frontend/summary-store.ts @@ -0,0 +1,115 @@ +import { useCallback, useSyncExternalStore } from "react"; +import type { ServerMetrics } from "../shared/metrics.js"; +import type { HostMetricsApi } from "./host-metrics-api"; + +const REFRESH_MS = 30_000; + +interface Entry { + refs: number; + viewerSessionId?: string; + data: ServerMetrics | null; + listeners: Set<() => void>; +} + +/** + * Latest metrics for hosts shown outside the tab (dashboard rows, homepage + * widgets). Each watched host holds one viewer, so polling runs while + * something shows it and stops with the last one. + */ +export function createMetricsSummaryStore(api: HostMetricsApi) { + const entries = new Map(); + let timer: ReturnType | null = null; + + const notify = (entry: Entry) => { + for (const listener of entry.listeners) listener(); + }; + + async function refresh(hostId: number): Promise { + const entry = entries.get(hostId); + if (!entry) return; + try { + if ( + entry.viewerSessionId && + !(await api.heartbeat(entry.viewerSessionId)) + ) { + entry.viewerSessionId = undefined; + } + if (!entry.viewerSessionId) { + const registered = await api.registerViewer(hostId); + if (registered.viewerSessionId) { + entry.viewerSessionId = registered.viewerSessionId; + } + } + entry.data = await api.getMetrics(hostId); + } catch { + // Keep the last value; the next tick tries again. + } + if (entries.get(hostId) === entry) notify(entry); + } + + function tick() { + if (typeof document !== "undefined" && document.hidden) return; + for (const hostId of entries.keys()) void refresh(hostId); + } + + function release(hostId: number, entry: Entry) { + entries.delete(hostId); + if (entry.viewerSessionId) { + void api.unregisterViewer(hostId, entry.viewerSessionId).catch(() => {}); + } + if (entries.size === 0 && timer) { + clearInterval(timer); + timer = null; + } + } + + return { + watch(hostId: number, listener: () => void): () => void { + let entry = entries.get(hostId); + if (!entry) { + entry = { refs: 0, data: null, listeners: new Set() }; + entries.set(hostId, entry); + void refresh(hostId); + } + entry.refs++; + entry.listeners.add(listener); + if (!timer) timer = setInterval(tick, REFRESH_MS); + const held = entry; + return () => { + held.listeners.delete(listener); + held.refs--; + if (held.refs <= 0) release(hostId, held); + }; + }, + + get(hostId: number): ServerMetrics | null { + return entries.get(hostId)?.data ?? null; + }, + + dispose() { + for (const [hostId, entry] of [...entries.entries()]) { + release(hostId, entry); + } + }, + }; +} + +export type MetricsSummaryStore = ReturnType; + +/** The host's latest sample, polled while the calling component is shown. */ +export function useMetricsSummary( + store: MetricsSummaryStore, + hostId: number | null | undefined, +): ServerMetrics | null { + // Stable per host, or every render would drop and re-add the viewer. + const subscribe = useCallback( + (onChange: () => void) => + hostId ? store.watch(hostId, onChange) : () => {}, + [store, hostId], + ); + return useSyncExternalStore( + subscribe, + () => (hostId ? store.get(hostId) : null), + () => null, + ); +} diff --git a/plugins/host-metrics/src/frontend/temperature-preference.ts b/plugins/host-metrics/src/frontend/temperature-preference.ts index 6a787c431..f1963f81f 100644 --- a/plugins/host-metrics/src/frontend/temperature-preference.ts +++ b/plugins/host-metrics/src/frontend/temperature-preference.ts @@ -1,4 +1,6 @@ -import type { TemperatureSensor } from "@/types/stats-widgets"; +import type { TemperatureSensor } from "../shared/stats-widgets.js"; + +export type TemperatureUnit = "celsius" | "fahrenheit"; export function selectTemperatureSensor( sensors: TemperatureSensor[], @@ -7,6 +9,22 @@ export function selectTemperatureSensor( return sensors.find((sensor) => sensor.label === preferredLabel) ?? null; } +/** The sensor a person picked is remembered per browser. */ export function temperaturePreferenceKey(hostId: number) { return `termix-host-metrics:${hostId}:temperature-sensor`; } + +/** The temperatureUnit user setting, celsius for anything else. */ +export function temperatureUnitOf(value: unknown): TemperatureUnit { + return value === "fahrenheit" ? "fahrenheit" : "celsius"; +} + +export function formatTemperature( + celsius: number | null | undefined, + unit: TemperatureUnit = "celsius", +): string { + if (typeof celsius !== "number" || !Number.isFinite(celsius)) return "N/A"; + return unit === "fahrenheit" + ? `${((celsius * 9) / 5 + 32).toFixed(1)}°F` + : `${celsius.toFixed(1)}°C`; +} diff --git a/plugins/host-metrics/src/shared/homepage.ts b/plugins/host-metrics/src/shared/homepage.ts new file mode 100644 index 000000000..5468c024d --- /dev/null +++ b/plugins/host-metrics/src/shared/homepage.ts @@ -0,0 +1,31 @@ +import type { HomepageWidgetContribution } from "@termix/plugin-sdk/frontend"; + +/** The homepage's grid step, in pixels. */ +export const GRID_SIZE = 30; + +export type MetricsChartMetric = + "cpu" | "memory" | "disk" | "net_rx" | "net_tx"; +export type MetricsChartRange = "15m" | "1h" | "6h" | "24h"; + +export interface MetricsChartConfig { + hostId: number; + metric: MetricsChartMetric; + range: MetricsChartRange; + showCurrentValue: boolean; +} + +/** What the homepage hands a widget; only the fields this plugin reads. */ +export interface WidgetComponentProps { + widget: { title?: string | null }; + config: C; +} + +export interface WidgetEditFormProps { + config: C; + onChange: (config: C) => void; +} + +export type WidgetDefinition = Omit< + HomepageWidgetContribution, + "defaultConfig" +> & { defaultConfig: C }; diff --git a/src/types/host-metrics.ts b/plugins/host-metrics/src/shared/host-metrics.ts similarity index 94% rename from src/types/host-metrics.ts rename to plugins/host-metrics/src/shared/host-metrics.ts index ccbcc9d10..1ec2901f1 100644 --- a/src/types/host-metrics.ts +++ b/plugins/host-metrics/src/shared/host-metrics.ts @@ -4,7 +4,7 @@ import type { WidgetType } from "./stats-widgets.js"; * Host Metrics layout model. Shared by the frontend tab and the backend * preferences endpoint. Metric card ids reuse the existing `WidgetType` values * verbatim (so `enabledWidgets` stays valid for the mobile app); manager card - * ids are new and never enter `statsConfig.enabledWidgets`. + * ids are new and never enter the `enabledWidgets` host setting. */ export type HostMetricCardId = WidgetType; @@ -85,7 +85,7 @@ export function isManagerCardId(id: string): id is HostMetricManagerId { /** * Derive the flat `enabledWidgets` array (canonical order, deduped, metric ids * only) from a set of layout slots. This is the single source of truth keeping - * `statsConfig.enabledWidgets` valid for the mobile app. + * the `enabledWidgets` host setting valid for the mobile app. */ export function deriveEnabledWidgets( slots: Array<{ id: string }>, @@ -117,7 +117,7 @@ const DEFAULT_HEIGHT: Partial> = {}; /** * Build a default layout from a list of enabled metric-card ids (e.g. the host's - * current `statsConfig.enabledWidgets`), preserving canonical order. Manager + * current the `enabledWidgets` host setting), preserving canonical order. Manager * cards are not added by default. */ export function defaultLayoutFromWidgets( diff --git a/plugins/host-metrics/src/shared/metrics.ts b/plugins/host-metrics/src/shared/metrics.ts new file mode 100644 index 000000000..e261a973c --- /dev/null +++ b/plugins/host-metrics/src/shared/metrics.ts @@ -0,0 +1,136 @@ +import type { GpuMetrics } from "./stats-widgets.js"; + +/** What GET /metrics/:id answers. */ +export interface CpuMetrics { + percent: number | null; + cores: number | null; + load: [number, number, number] | null; +} + +export interface MemoryMetrics { + percent: number | null; + usedGiB: number | null; + totalGiB: number | null; +} + +export interface DiskFilesystem { + filesystem: string; + type: string; + mount: string; + percent: number | null; + usedHuman: string | null; + totalHuman: string | null; + availableHuman: string | null; + usedBytes: number | null; + totalBytes: number | null; + availableBytes: number | null; + label?: string; +} + +interface DiskMetrics { + percent: number | null; + usedHuman: string | null; + totalHuman: string | null; + availableHuman?: string | null; + mount?: string | null; + filesystems?: DiskFilesystem[]; +} + +export interface NetworkInterface { + name: string; + ip: string; + state: string; + rx?: string | null; + tx?: string | null; + rxBytes?: string | null; + txBytes?: string | null; + rxRateBps?: number | null; + txRateBps?: number | null; +} + +export interface ProcessInfo { + pid: string; + user: string; + cpu: string; + mem: string; + command: string; +} + +export interface LoginRecord { + user: string; + ip: string; + time: string; + status: "success" | "failed"; +} + +export interface ListeningPort { + protocol: "tcp" | "udp"; + localAddress: string; + localPort: number; + state?: string; + pid?: number; + process?: string; +} + +export interface FirewallRule { + chain: string; + target: string; + protocol: string; + source: string; + destination: string; + dport?: string; + sport?: string; + state?: string; + interface?: string; + extra?: string; +} + +export interface FirewallChain { + name: string; + policy: string; + rules: FirewallRule[]; +} + +export type ServerMetrics = { + cpu: CpuMetrics; + memory: MemoryMetrics; + disk: DiskMetrics; + network?: { interfaces?: NetworkInterface[] }; + uptime?: { seconds?: number | null; formatted?: string | null }; + system?: { + hostname?: string | null; + os?: string | null; + kernel?: string | null; + arch?: string | null; + }; + processes?: { + total?: number | null; + running?: number | null; + top?: ProcessInfo[]; + }; + login_stats?: { + recentLogins?: LoginRecord[]; + failedLogins?: LoginRecord[]; + totalLogins?: number; + uniqueIPs?: number; + }; + ports?: { + source?: "ss" | "netstat" | "none"; + ports?: ListeningPort[]; + }; + firewall?: { + type?: "iptables" | "nftables" | "none"; + status?: "active" | "inactive" | "unknown"; + chains?: FirewallChain[]; + }; + temperature?: { + source?: "sysfs" | "sensors" | "none"; + highestCelsius?: number | null; + sensors?: Array<{ + label: string; + celsius: number; + }>; + }; + gpu?: GpuMetrics; + lastChecked: string; +}; diff --git a/src/types/stats-widgets.ts b/plugins/host-metrics/src/shared/stats-widgets.ts similarity index 51% rename from src/types/stats-widgets.ts rename to plugins/host-metrics/src/shared/stats-widgets.ts index 266a241d7..aec7b6631 100644 --- a/src/types/stats-widgets.ts +++ b/plugins/host-metrics/src/shared/stats-widgets.ts @@ -90,40 +90,72 @@ export interface GpuMetrics { processes: GpuProcess[]; } -export interface StatsConfig { - enabledWidgets: WidgetType[]; - statusCheckEnabled: boolean; - statusCheckInterval: number; - useGlobalStatusInterval?: boolean; +/** This plugin's host settings, as core stores them in plugin_settings. */ +export interface HostMetricsSettings { metricsEnabled: boolean; - metricsInterval: number; - useGlobalMetricsInterval?: boolean; - disableTcpPing?: boolean; + /** Seconds between samples; null follows the admin setting. */ + metricsInterval: number | null; + enabledWidgets: WidgetType[]; /** Filesystem mount points to leave out of the disk widget. */ - excludedMounts?: string[]; + excludedMounts: string[]; /** Extra paths to monitor, including paths inside bind-mounted containers. */ - monitoredMounts?: Array<{ path: string; label?: string }>; + monitoredMounts: Array<{ path: string; label?: string }>; } -export const DEFAULT_STATS_CONFIG: StatsConfig = { - enabledWidgets: [ - "cpu", - "memory", - "disk", - "network", - "uptime", - "system", - "login_stats", - "processes", - "ports", - "firewall", - "temperature", - ], - statusCheckEnabled: true, - statusCheckInterval: 30, - useGlobalStatusInterval: true, +export const DEFAULT_ENABLED_WIDGETS: WidgetType[] = [ + "cpu", + "memory", + "disk", + "network", + "uptime", + "system", + "login_stats", + "processes", + "ports", + "firewall", + "temperature", +]; + +export const DEFAULT_HOST_METRICS_SETTINGS: HostMetricsSettings = { metricsEnabled: true, - metricsInterval: 30, - useGlobalMetricsInterval: true, - disableTcpPing: false, + metricsInterval: null, + enabledWidgets: DEFAULT_ENABLED_WIDGETS, + excludedMounts: [], + monitoredMounts: [], }; + +/** Host settings with defaults filled in and bad values dropped. */ +export function readHostMetricsSettings( + values: Record | null | undefined, +): HostMetricsSettings { + const bag = values ?? {}; + const interval = Number(bag.metricsInterval); + return { + metricsEnabled: bag.metricsEnabled !== false, + metricsInterval: + bag.metricsInterval != null && + Number.isInteger(interval) && + interval >= 5 && + interval <= 3600 + ? interval + : null, + enabledWidgets: Array.isArray(bag.enabledWidgets) + ? (bag.enabledWidgets.filter( + (widget) => typeof widget === "string", + ) as WidgetType[]) + : DEFAULT_ENABLED_WIDGETS, + excludedMounts: Array.isArray(bag.excludedMounts) + ? bag.excludedMounts.filter( + (mount): mount is string => typeof mount === "string", + ) + : [], + monitoredMounts: Array.isArray(bag.monitoredMounts) + ? (bag.monitoredMounts as unknown[]).filter( + (mount): mount is { path: string; label?: string } => + !!mount && + typeof mount === "object" && + typeof (mount as { path?: unknown }).path === "string", + ) + : [], + }; +} diff --git a/plugins/host-metrics/tests/backend/activate.test.ts b/plugins/host-metrics/tests/backend/activate.test.ts new file mode 100644 index 000000000..f2864491b --- /dev/null +++ b/plugins/host-metrics/tests/backend/activate.test.ts @@ -0,0 +1,120 @@ +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 type { MetricsViewersV1 } from "../../src/backend/index.js"; +import { hostImportNormalizer } from "../../src/backend/host-import.js"; +import { manifest, pluginDir, startServer, type TestServer } from "./server"; + +let db: TestDb | null = null; +let server: TestServer | null = null; + +afterEach(async () => { + db?.close(); + db = null; + await server?.close(); + server = null; +}); + +describe("host-metrics activate", () => { + it.each(["db:own", "network:serve"])( + "fails closed without %s", + async (missing) => { + db = await createTestDb(pluginDir); + const mock = createMockCtx({ + pluginId: manifest.id, + manifest, + capabilities: manifest.capabilities.filter((cap) => cap !== missing), + db: db.database, + router: () => express.Router(), + }); + await expect(activate(mock.ctx)).rejects.toBeInstanceOf( + PluginCapabilityError, + ); + }, + ); + + it("offers viewers as a service that runs as the caller", async () => { + server = await startServer(); + const viewers = server.mock.services.get( + "host-metrics.viewers", + ) as MetricsViewersV1; + + const registered = await server.mock.ctx.asUser("user-1", () => + viewers.register(7), + ); + expect(registered).toEqual({ viewerSessionId: expect.any(String) }); + const id = (registered as { viewerSessionId: string }).viewerSessionId; + + expect( + await server.mock.ctx.asUser("user-1", async () => viewers.heartbeat(id)), + ).toBe(true); + expect( + await server.mock.ctx.asUser("user-2", async () => viewers.heartbeat(id)), + ).toBe(false); + + expect( + await server.mock.ctx.asUser("user-1", () => viewers.register(99)), + ).toEqual({ skipped: true, reason: "host_not_found" }); + }); + + it("stops every timer when the plugin is disabled", async () => { + server = await startServer(); + const jobs = server.mock.scheduled; + expect(jobs.length).toBeGreaterThan(0); + for (const dispose of server.mock.disposals) await dispose(); + expect(jobs.every((job) => job.stopped)).toBe(true); + }); + + it("registers its host import normalizer", async () => { + server = await startServer(); + expect( + server.mock.ctx.registry.consume("host-metrics.hostImportNormalizer"), + ).toBe(hostImportNormalizer); + }); +}); + +describe("hostImportNormalizer", () => { + it("reads this plugin's settings from a Termix export", () => { + expect( + hostImportNormalizer({ + pluginSettings: { + "host-metrics": { metricsEnabled: false, enabledWidgets: ["cpu"] }, + }, + }), + ).toMatchObject({ metricsEnabled: false, enabledWidgets: ["cpu"] }); + }); + + it("reads the metrics half of an export from before 2.9.0", () => { + expect( + hostImportNormalizer({ + statsConfig: { + statusCheckEnabled: false, + metricsEnabled: true, + metricsInterval: 45, + useGlobalMetricsInterval: false, + excludedMounts: ["/snap"], + }, + }), + ).toMatchObject({ + metricsEnabled: true, + metricsInterval: 45, + excludedMounts: ["/snap"], + }); + }); + + it("ignores the old interval while the global one was in use", () => { + expect( + hostImportNormalizer({ statsConfig: { metricsInterval: 45 } }), + ).toMatchObject({ metricsInterval: null }); + }); + + it("writes nothing for a row without metrics settings", () => { + expect(hostImportNormalizer({ name: "x" })).toBeNull(); + }); +}); diff --git a/plugins/host-metrics/tests/backend/helpers.test.ts b/plugins/host-metrics/tests/backend/helpers.test.ts index c9d7b0365..114fc482e 100644 --- a/plugins/host-metrics/tests/backend/helpers.test.ts +++ b/plugins/host-metrics/tests/backend/helpers.test.ts @@ -1,82 +1,53 @@ -import type { Client } from "ssh2"; -import { afterAll, beforeAll, describe, it, expect, vi } from "vitest"; +import { describe, it, expect } from "vitest"; import { + connectionLog, + sudoPasswordOf, supportsMetrics, - isTcpPingEnabled, - parseStatusHostIds, - tcpPingThroughJumpHost, + type MetricsHost, } from "../../src/backend/helpers.js"; -import { createConnectionLog } from "../../../../src/backend/hosts/connection-log.js"; import { createFakeContext } from "@termix/plugin-sdk/testing"; -import { setPluginSsh } from "../../src/backend/ssh.js"; + +const ssh = createFakeContext().ctx.ssh; describe("supportsMetrics", () => { - beforeAll(() => { - setPluginSsh(createFakeContext().ctx.ssh); - }); - afterAll(() => { - setPluginSsh(null); - }); - it("supports plain ssh hosts", () => { expect( - supportsMetrics({ connectionType: "ssh", authType: "password" }), + supportsMetrics({ connectionType: "ssh", authType: "password" }, ssh), ).toBe(true); }); it("defaults missing connectionType to ssh", () => { - expect(supportsMetrics({ authType: "key" })).toBe(true); + expect(supportsMetrics({ authType: "key" }, ssh)).toBe(true); }); - it("rejects non-ssh connection types", () => { - expect(supportsMetrics({ connectionType: "rdp" })).toBe(false); - expect(supportsMetrics({ connectionType: "vnc" })).toBe(false); - expect(supportsMetrics({ connectionType: "telnet" })).toBe(false); + it("rejects remote desktop only hosts", () => { + expect(supportsMetrics({ connectionType: "rdp" }, ssh)).toBe(false); + expect(supportsMetrics({ connectionType: "vnc" }, ssh)).toBe(false); + expect(supportsMetrics({ connectionType: "telnet" }, ssh)).toBe(false); }); - it("rejects ssh hosts that cannot run shell commands", () => { - expect(supportsMetrics({ connectionType: "ssh", authType: "none" })).toBe( - false, - ); - expect(supportsMetrics({ connectionType: "ssh", authType: "opkssh" })).toBe( - false, - ); - }); -}); - -describe("isTcpPingEnabled", () => { - it("is enabled when status checks are on and tcp ping is not disabled", () => { + it("accepts a remote desktop host that also has ssh on", () => { expect( - isTcpPingEnabled({ statusCheckEnabled: true, disableTcpPing: false }), + supportsMetrics( + { connectionType: "rdp", enableSsh: true, authType: "password" }, + ssh, + ), ).toBe(true); - expect(isTcpPingEnabled({ statusCheckEnabled: true })).toBe(true); }); - it("is disabled when status checks are off", () => { - expect(isTcpPingEnabled({ statusCheckEnabled: false })).toBe(false); - }); - - it("is disabled when tcp ping is explicitly disabled", () => { + it("rejects ssh hosts that cannot connect unattended", () => { expect( - isTcpPingEnabled({ statusCheckEnabled: true, disableTcpPing: true }), + supportsMetrics({ connectionType: "ssh", authType: "none" }, ssh), + ).toBe(false); + expect( + supportsMetrics({ connectionType: "ssh", authType: "opkssh" }, ssh), ).toBe(false); }); }); -describe("parseStatusHostIds", () => { - it("distinguishes an unrestricted request from an empty host set", () => { - expect(parseStatusHostIds(undefined)).toBeNull(); - expect(parseStatusHostIds("")).toEqual(new Set()); - }); - - it("keeps only valid positive host IDs", () => { - expect(parseStatusHostIds("7,2,7,-1,nope,1.5")).toEqual(new Set([7, 2])); - }); -}); - -describe("createConnectionLog", () => { - it("builds a log entry without id/timestamp", () => { - const entry = createConnectionLog("info", "connection", "Connecting", { +describe("connectionLog", () => { + it("builds a log entry without id or timestamp", () => { + const entry = connectionLog("info", "connection", "Connecting", { hostId: 1, }); expect(entry).toEqual({ @@ -85,61 +56,32 @@ describe("createConnectionLog", () => { message: "Connecting", details: { hostId: 1 }, }); - expect("id" in entry).toBe(false); - expect("timestamp" in entry).toBe(false); }); }); -describe("tcpPingThroughJumpHost", () => { - it("reports the final destination online when forwarding succeeds", async () => { - const stream = { destroy: vi.fn() }; - const jumpClient = { - end: vi.fn(), - forwardOut: vi.fn((_src, _srcPort, host, port, callback) => { - expect(host).toBe("private.example"); - expect(port).toBe(22); - callback(undefined, stream); - }), - } as unknown as Pick; +describe("sudoPasswordOf", () => { + const host = (extra: Partial) => + ({ + id: 1, + ip: "h", + port: 22, + username: "u", + userId: "o", + ...extra, + }) as MetricsHost; - await expect( - tcpPingThroughJumpHost(jumpClient, "private.example", 22), - ).resolves.toBe(true); - expect(stream.destroy).toHaveBeenCalledOnce(); - expect(jumpClient.end).toHaveBeenCalledOnce(); + it("prefers the host's sudo password", () => { + expect( + sudoPasswordOf( + host({ sudoPassword: "a", terminalConfig: { sudoPassword: "b" } }), + ), + ).toBe("a"); }); - it("reports the final destination offline when forwarding fails", async () => { - const jumpClient = { - end: vi.fn(), - forwardOut: vi.fn((_src, _srcPort, _host, _port, callback) => { - callback(new Error("Connection refused")); - }), - } as unknown as Pick; - - await expect( - tcpPingThroughJumpHost(jumpClient, "private.example", 22), - ).resolves.toBe(false); - expect(jumpClient.end).toHaveBeenCalledOnce(); - }); - - it("reports the final destination offline when forwarding times out", async () => { - vi.useFakeTimers(); - const jumpClient = { - end: vi.fn(), - forwardOut: vi.fn(), - } as unknown as Pick; - - const result = tcpPingThroughJumpHost( - jumpClient, - "private.example", - 22, - 5000, - ); - await vi.advanceTimersByTimeAsync(5000); - - await expect(result).resolves.toBe(false); - expect(jumpClient.end).toHaveBeenCalledOnce(); - vi.useRealTimers(); + it("falls back to the terminal config", () => { + expect( + sudoPasswordOf(host({ terminalConfig: { sudoPassword: "b" } })), + ).toBe("b"); + expect(sudoPasswordOf(host({}))).toBeUndefined(); }); }); diff --git a/plugins/host-metrics/tests/backend/host-metrics-preferences.test.ts b/plugins/host-metrics/tests/backend/host-metrics-preferences.test.ts index c46aff27c..ab0e1c5c3 100644 --- a/plugins/host-metrics/tests/backend/host-metrics-preferences.test.ts +++ b/plugins/host-metrics/tests/backend/host-metrics-preferences.test.ts @@ -5,7 +5,7 @@ import { isMetricCardId, isManagerCardId, METRIC_CARD_IDS, -} from "../../../../src/types/host-metrics.js"; +} from "../../src/shared/host-metrics.js"; describe("deriveEnabledWidgets", () => { it("returns only metric ids, in canonical order, deduped", () => { diff --git a/plugins/host-metrics/tests/backend/login-alert.test.ts b/plugins/host-metrics/tests/backend/login-alert.test.ts deleted file mode 100644 index 7e1569aad..000000000 --- a/plugins/host-metrics/tests/backend/login-alert.test.ts +++ /dev/null @@ -1,84 +0,0 @@ -/** - * SSH login alerts reach automations. - * - * This used to be POST /internal/login-alert on this plugin's own port, - * guarded by a localhost IP check plus a shared internal token, and the tests - * here covered both of those plus the middleware ordering bug that once made - * every alert 401. A4 replaced the call with an in-process event, so the - * transport, the shared secret and the ordering hazard are all gone; what is - * left to assert is that the handler forwards the right thing and ignores a - * payload it cannot use. - * - * The route module pulls in the whole metrics stack, so this extracts the - * handler from source rather than importing it, the same way the old test did. - */ - -import { readFileSync } from "node:fs"; -import { runInNewContext } from "node:vm"; -import ts from "typescript"; -import { beforeEach, expect, it, vi } from "vitest"; - -const source = readFileSync( - new URL("../../src/backend/routes.ts", import.meta.url), - "utf8", -); -const ast = ts.createSourceFile( - "routes.ts", - source, - ts.ScriptTarget.Latest, - true, -); - -const declaration = ast.statements.find( - (node) => - ts.isFunctionDeclaration(node) && node.name?.getText(ast) === "onHostLogin", -); -if (!declaration) throw new Error("onHostLogin is missing"); - -const notify = vi.fn(); -const context: Record = { - notifyAutomationInternalEvent: notify, - exports: {}, -}; - -runInNewContext( - ts.transpileModule(declaration.getText(ast), { - compilerOptions: { - module: ts.ModuleKind.CommonJS, - target: ts.ScriptTarget.ES2022, - }, - }).outputText, - context, -); - -const onHostLogin = context.onHostLogin as (payload: { - hostId: number; - userId: string; - sshUser: string; - fromIp: string; -}) => void; - -beforeEach(() => vi.clearAllMocks()); - -it("delivers successful SSH login details to automations", () => { - onHostLogin({ - hostId: 42, - userId: "owner", - sshUser: "alice", - fromIp: "192.0.2.4", - }); - - expect(notify).toHaveBeenCalledExactlyOnceWith("user_login", "owner", 42, { - sshUser: "alice", - fromIp: "192.0.2.4", - }); -}); - -it.each([ - ["no host", { hostId: 0, userId: "owner", sshUser: "a", fromIp: "1.2.3.4" }], - ["no user", { hostId: 42, userId: "", sshUser: "a", fromIp: "1.2.3.4" }], -])("ignores an unusable payload (%s)", (_label, payload) => { - onHostLogin(payload as Parameters[0]); - - expect(notify).not.toHaveBeenCalled(); -}); diff --git a/plugins/host-metrics/tests/backend/managers/managers.test.ts b/plugins/host-metrics/tests/backend/managers/managers.test.ts index 6a9d97118..9e0d10bfa 100644 --- a/plugins/host-metrics/tests/backend/managers/managers.test.ts +++ b/plugins/host-metrics/tests/backend/managers/managers.test.ts @@ -2,8 +2,11 @@ import { describe, it, expect } from "vitest"; import { buildSudoCommand, shellSingleQuote, -} from "../../../../../src/backend/hosts/metrics-shared/exec-elevated.js"; -import { parsePlatformProbe } from "../../../../../src/backend/hosts/metrics-shared/platform.js"; + parsePlatformProbe, + buildPackageActionCommand, + parseUpgradable, + buildListUpgradableCommand, +} from "@termix/plugin-sdk/host-commands"; import { isValidSystemdUnit, isValidPid, @@ -15,7 +18,7 @@ import { isValidSignal, isValidServiceAction, isAllowedPath, -} from "../../../../../src/backend/hosts/metrics-shared/validation.js"; +} from "../../../src/backend/managers/validation.js"; import { parseServiceList, buildServiceActionCommand, @@ -34,11 +37,6 @@ import { isValidCronSchedule, buildApplyCrontabCommand, } from "../../../src/backend/managers/cron.js"; -import { - buildPackageActionCommand, - parseUpgradable, - buildListUpgradableCommand, -} from "../../../../../src/backend/hosts/metrics-shared/package-commands.js"; import { buildIssueCommand, buildRenewCommand, diff --git a/plugins/host-metrics/tests/backend/metrics-shared/common-utils.test.ts b/plugins/host-metrics/tests/backend/metrics-shared/common-utils.test.ts deleted file mode 100644 index bf05a9ecb..000000000 --- a/plugins/host-metrics/tests/backend/metrics-shared/common-utils.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { - toFixedNum, - kibToGiB, - encodePowerShellCommand, -} from "../../../../../src/backend/hosts/metrics-shared/common-utils.js"; - -describe("toFixedNum", () => { - it("rounds to the requested digit count", () => { - expect(toFixedNum(3.14159, 2)).toBe(3.14); - expect(toFixedNum(3.14159, 0)).toBe(3); - expect(toFixedNum(2.5, 0)).toBe(3); - }); - - it("defaults to 2 digits", () => { - expect(toFixedNum(1.23456)).toBe(1.23); - }); - - it("returns null for non-finite or non-number input", () => { - expect(toFixedNum(null)).toBeNull(); - expect(toFixedNum(undefined)).toBeNull(); - expect(toFixedNum(NaN)).toBeNull(); - expect(toFixedNum(Infinity)).toBeNull(); - }); -}); - -describe("kibToGiB", () => { - it("converts kibibytes to gibibytes", () => { - expect(kibToGiB(1024 * 1024)).toBe(1); - expect(kibToGiB(0)).toBe(0); - expect(kibToGiB(2 * 1024 * 1024)).toBe(2); - }); -}); - -describe("encodePowerShellCommand", () => { - it("UTF-16LE base64 encodes the script, decodable back to the source", () => { - const script = "Write-Output 'hello'"; - const encoded = encodePowerShellCommand(script); - expect(Buffer.from(encoded, "base64").toString("utf16le")).toBe(script); - }); -}); diff --git a/plugins/host-metrics/tests/backend/metrics-shared/exec-elevated.test.ts b/plugins/host-metrics/tests/backend/metrics-shared/exec-elevated.test.ts deleted file mode 100644 index b0e1d5c80..000000000 --- a/plugins/host-metrics/tests/backend/metrics-shared/exec-elevated.test.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; - -const execCommand = vi.fn(); -vi.mock( - "../../../../../src/backend/hosts/metrics-shared/common-utils.js", - () => ({ - execCommand: (...args: unknown[]) => execCommand(...args), - }), -); - -import { - execElevated, - ElevationError, -} from "../../../../../src/backend/hosts/metrics-shared/exec-elevated.js"; -import type { Client } from "ssh2"; - -const fakeClient = {} as Client; - -function result(stdout: string, stderr = "", code: number | null = 0) { - return { stdout, stderr, code }; -} - -beforeEach(() => { - execCommand.mockReset(); -}); - -describe("execElevated (no force)", () => { - it("returns the direct result when the command succeeds unprivileged", async () => { - execCommand.mockResolvedValueOnce(result("hello", "", 0)); - const r = await execElevated(fakeClient, "echo hello", "pw"); - expect(r.usedSudo).toBe(false); - expect(r.stdout).toBe("hello"); - expect(execCommand).toHaveBeenCalledTimes(1); - }); - - it("does NOT escalate when stdout merely contains scary words", async () => { - // A non-zero exit whose OUTPUT contains 'permission denied' but stderr does - // not should be surfaced as-is, never retried under sudo. - execCommand.mockResolvedValueOnce( - result("log line: permission denied for user foo", "", 1), - ); - const r = await execElevated(fakeClient, "grep denied /tmp/app.log", "pw"); - expect(r.usedSudo).toBe(false); - expect(execCommand).toHaveBeenCalledTimes(1); - }); - - it("escalates when stderr indicates a permission problem", async () => { - execCommand - .mockResolvedValueOnce(result("", "Permission denied", 1)) - .mockResolvedValueOnce(result("__TX_SUDO_OK__\nelevated output", "", 0)); - const r = await execElevated(fakeClient, "cat /etc/shadow", "pw"); - expect(r.usedSudo).toBe(true); - expect(r.stdout).toBe("elevated output"); - expect(execCommand).toHaveBeenCalledTimes(2); - }); - - it("throws SUDO_REQUIRED when elevation is needed but no password is set", async () => { - execCommand.mockResolvedValueOnce(result("", "Permission denied", 1)); - await expect( - execElevated(fakeClient, "cat /etc/shadow", undefined), - ).rejects.toMatchObject({ code: "SUDO_REQUIRED" }); - }); -}); - -describe("execElevated (forced)", () => { - it("strips the success marker from stdout", async () => { - execCommand.mockResolvedValueOnce( - result("__TX_SUDO_OK__\nthe real output\n", "", 0), - ); - const r = await execElevated(fakeClient, "id", "pw", { forceSudo: true }); - expect(r.stdout).toBe("the real output\n"); - expect(r.usedSudo).toBe(true); - }); - - it("does NOT throw when command output contains 'incorrect password' but sudo authenticated", async () => { - execCommand.mockResolvedValueOnce( - result( - "__TX_SUDO_OK__\nUser entered an incorrect password earlier", - "", - 0, - ), - ); - const r = await execElevated(fakeClient, "tail /var/log/auth.log", "pw", { - forceSudo: true, - }); - expect(r.usedSudo).toBe(true); - expect(r.stdout).toContain("incorrect password"); - }); - - it("throws SUDO_FAILED on a real wrong-password (no marker, sudo stderr)", async () => { - execCommand.mockResolvedValueOnce( - result("", "sudo: 1 incorrect password attempt", 1), - ); - await expect( - execElevated(fakeClient, "id", "wrong", { forceSudo: true }), - ).rejects.toMatchObject({ code: "SUDO_FAILED" }); - }); - - it("throws NOT_SUDOER when the user is not in sudoers", async () => { - execCommand.mockResolvedValueOnce( - result("", "deploy is not in the sudoers file.", 1), - ); - await expect( - execElevated(fakeClient, "id", "pw", { forceSudo: true }), - ).rejects.toMatchObject({ code: "NOT_SUDOER" }); - }); - - it("throws SUDO_REQUIRED when forced without a password", async () => { - await expect( - execElevated(fakeClient, "id", undefined, { forceSudo: true }), - ).rejects.toBeInstanceOf(ElevationError); - expect(execCommand).not.toHaveBeenCalled(); - }); -}); diff --git a/plugins/host-metrics/tests/backend/migrations.test.ts b/plugins/host-metrics/tests/backend/migrations.test.ts new file mode 100644 index 000000000..a1b65c675 --- /dev/null +++ b/plugins/host-metrics/tests/backend/migrations.test.ts @@ -0,0 +1,193 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { createTestDb, type TestDb } from "@termix/plugin-sdk/testing"; +import { pluginDir } from "./server"; + +// The four tables as core's SQLite bootstrap created them before 2.9.0. +const LEGACY_DDL = ` + CREATE TABLE host_metrics_preferences ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT NOT NULL, + host_id INTEGER NOT NULL, + layout TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, + FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE + ); + CREATE UNIQUE INDEX idx_host_metrics_prefs_user_host + ON host_metrics_preferences (user_id, host_id); + + CREATE TABLE host_health_checks ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT NOT NULL, + host_id INTEGER NOT NULL, + checks TEXT NOT NULL, + interval_seconds INTEGER NOT NULL DEFAULT 300, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, + FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE + ); + CREATE UNIQUE INDEX idx_host_health_checks_user_host + ON host_health_checks (user_id, host_id); + + CREATE TABLE host_health_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT NOT NULL, + host_id INTEGER NOT NULL, + check_id TEXT NOT NULL, + ts TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + ok INTEGER NOT NULL, + latency_ms INTEGER, + detail TEXT, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, + FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE + ); + CREATE INDEX idx_host_health_history_lookup + ON host_health_history (user_id, host_id, check_id, ts); + + CREATE TABLE host_metrics_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + host_id INTEGER NOT NULL REFERENCES ssh_data(id) ON DELETE CASCADE, + ts TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + cpu_percent REAL, + mem_percent REAL, + disk_percent REAL, + net_rx_bytes INTEGER, + net_tx_bytes INTEGER + ); + CREATE INDEX idx_host_metrics_history_host_ts + ON host_metrics_history (host_id, ts DESC); +`; + +const LEGACY = [ + "host_metrics_preferences", + "host_health_checks", + "host_health_history", + "host_metrics_history", +]; + +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 indexNames = (table: string) => + ( + db!.sqlite + .prepare( + "SELECT name FROM sqlite_master WHERE type = 'index' AND tbl_name = ? AND name NOT LIKE 'sqlite_%'", + ) + .all(table) as Array<{ name: string }> + ) + .map((row) => row.name) + .sort(); + +const seedLegacy = (sqlite: TestDb["sqlite"]) => { + sqlite.exec("INSERT INTO users (id) VALUES ('user-1')"); + sqlite.exec("INSERT INTO ssh_data (id) VALUES (7)"); + sqlite.exec(LEGACY_DDL); + sqlite.exec(` + INSERT INTO host_metrics_preferences (user_id, host_id, layout) + VALUES ('user-1', 7, '{"slots":[],"columns":3}'); + INSERT INTO host_health_checks (user_id, host_id, checks, interval_seconds) + VALUES ('user-1', 7, '[]', 600); + INSERT INTO host_health_history (user_id, host_id, check_id, ts, ok, latency_ms, detail) + VALUES ('user-1', 7, 'web', '2026-09-01 10:00:00', 1, 12, 'HTTP 200'); + INSERT INTO host_metrics_history (host_id, ts, cpu_percent, mem_percent, disk_percent, net_rx_bytes, net_tx_bytes) + VALUES (7, '2026-09-01 10:00:00', 12.5, 40, 70, 100, 200); + `); +}; + +describe("adopting the host metrics tables", () => { + it("keeps every row and index when the legacy tables are there", async () => { + db = await createTestDb(pluginDir, { before: seedLegacy }); + + expect(db.applied).toEqual(["0001_adopt_host_metrics_tables"]); + for (const table of LEGACY) { + expect(tableExists(table)).toBe(false); + expect(tableExists(`p_host_metrics_${table}`)).toBe(true); + } + + expect( + db.sqlite + .prepare( + "SELECT user_id, host_id, layout FROM p_host_metrics_host_metrics_preferences", + ) + .all(), + ).toEqual([ + { user_id: "user-1", host_id: 7, layout: '{"slots":[],"columns":3}' }, + ]); + expect( + db.sqlite + .prepare( + "SELECT checks, interval_seconds FROM p_host_metrics_host_health_checks", + ) + .all(), + ).toEqual([{ checks: "[]", interval_seconds: 600 }]); + expect( + db.sqlite + .prepare( + "SELECT check_id, ok, latency_ms, detail FROM p_host_metrics_host_health_history", + ) + .all(), + ).toEqual([{ check_id: "web", ok: 1, latency_ms: 12, detail: "HTTP 200" }]); + expect( + db.sqlite + .prepare( + "SELECT ts, cpu_percent, net_tx_bytes FROM p_host_metrics_host_metrics_history", + ) + .all(), + ).toEqual([ + { ts: "2026-09-01 10:00:00", cpu_percent: 12.5, net_tx_bytes: 200 }, + ]); + + expect(indexNames("p_host_metrics_host_metrics_preferences")).toEqual([ + "idx_host_metrics_prefs_user_host", + ]); + expect(indexNames("p_host_metrics_host_health_checks")).toEqual([ + "idx_host_health_checks_user_host", + ]); + expect(indexNames("p_host_metrics_host_health_history")).toEqual([ + "idx_host_health_history_lookup", + ]); + expect(indexNames("p_host_metrics_host_metrics_history")).toEqual([ + "idx_host_metrics_history_host_ts", + ]); + }); + + it("creates the tables fresh when there are no legacy ones", async () => { + db = await createTestDb(pluginDir); + for (const table of LEGACY) { + expect(tableExists(table)).toBe(false); + expect( + db.sqlite.prepare(`SELECT * FROM p_host_metrics_${table}`).all(), + ).toEqual([]); + } + }); + + it("cascades every table on host delete, and the user tables on user delete", async () => { + db = await createTestDb(pluginDir, { before: seedLegacy }); + + db.sqlite.exec("DELETE FROM users WHERE id = 'user-1'"); + for (const table of LEGACY.slice(0, 3)) { + expect( + db.sqlite.prepare(`SELECT * FROM p_host_metrics_${table}`).all(), + ).toEqual([]); + } + + db.sqlite.exec("DELETE FROM ssh_data WHERE id = 7"); + expect( + db.sqlite + .prepare("SELECT * FROM p_host_metrics_host_metrics_history") + .all(), + ).toEqual([]); + }); +}); diff --git a/plugins/host-metrics/tests/backend/poller.test.ts b/plugins/host-metrics/tests/backend/poller.test.ts new file mode 100644 index 000000000..89d0ed399 --- /dev/null +++ b/plugins/host-metrics/tests/backend/poller.test.ts @@ -0,0 +1,243 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { createFakeContext } from "@termix/plugin-sdk/testing"; +import type { MetricsHost } from "../../src/backend/helpers.js"; + +const collectMetrics = vi.fn(); +vi.mock("../../src/backend/collect.js", async (importOriginal) => ({ + ...(await importOriginal()), + collectMetrics: (...args: unknown[]) => collectMetrics(...args), +})); + +const { MetricsPoller, TOPIC_SNAPSHOT, TOPIC_STATUS } = + await import("../../src/backend/poller.js"); +const { createMetricsState } = await import("../../src/backend/state.js"); +const { createLogger } = await import("../../src/backend/log.js"); + +const sample = { + cpu: { percent: 10, cores: 2, load: null }, + memory: { percent: 20, usedGiB: 1, totalGiB: 4 }, + disk: { percent: 30, filesystems: [] }, + network: { interfaces: [{ rxBytes: "100", txBytes: "200" }] }, +}; + +function host(id: number): MetricsHost { + return { + id, + userId: "user-1", + ip: `10.0.0.${id}`, + port: 22, + username: "root", + authType: "password", + connectionType: "ssh", + } as MetricsHost; +} + +function setup(hostIds = [7]) { + const fake = createFakeContext({ + pluginId: "host-metrics", + actor: "user-1", + sshHosts: hostIds.map(host), + hostStatuses: Object.fromEntries( + hostIds.map((id) => [ + id, + { status: "reachable" as const, lastChecked: "now" }, + ]), + ), + }); + const samples: unknown[] = []; + const state = createMetricsState(); + const poller = new MetricsPoller({ + ctx: fake.ctx, + log: createLogger(fake.ctx.log), + repository: { + recordSample: async (input: unknown) => { + samples.push(input); + }, + } as never, + ssh: fake.ctx.ssh, + state, + sessions: {} as never, + collectors: {} as never, + }); + return { fake, poller, state, samples }; +} + +const flush = () => new Promise((resolve) => setTimeout(resolve, 10)); + +beforeEach(() => { + collectMetrics.mockReset(); +}); + +afterEach(() => { + vi.useRealTimers(); +}); + +describe("MetricsPoller", () => { + it("polls from the first viewer and reports a working login", async () => { + collectMetrics.mockImplementation(async (_deps, _host, _mounts, onAuth) => { + onAuth?.(); + return sample; + }); + const { fake, poller, samples } = setup(); + + poller.registerViewer(7, "viewer-a", "user-1"); + await flush(); + + expect(collectMetrics).toHaveBeenCalledOnce(); + expect(poller.getMetrics(7)?.data).toEqual(sample); + expect(fake.statusReports).toEqual([{ hostId: 7, ok: true }]); + expect(samples).toEqual([ + expect.objectContaining({ + hostId: 7, + cpuPercent: 10, + netRxBytes: 100, + netTxBytes: 200, + }), + ]); + expect(fake.emitted.map((event) => event.topic)).toEqual([ + TOPIC_STATUS, + TOPIC_SNAPSHOT, + ]); + expect(fake.emitted[0].payload).toMatchObject({ + hostId: 7, + state: "collecting", + previous: null, + }); + const job = fake.scheduled.find((entry) => entry.kind === "every"); + expect(job?.ms).toBe(30_000); + }); + + it("uses the host's own interval over the admin one", async () => { + collectMetrics.mockResolvedValue(sample); + const { fake, poller } = setup(); + await fake.ctx.settings.set("metricsInterval", 60); + await fake.ctx.settings.setHost(7, "metricsInterval", 15); + + poller.registerViewer(7, "viewer-a", "user-1"); + await flush(); + + expect(fake.scheduled.find((entry) => entry.kind === "every")?.ms).toBe( + 15_000, + ); + }); + + it("waits for a host core sees as offline", async () => { + collectMetrics.mockResolvedValue(sample); + const { fake, poller } = setup(); + fake.hostStatuses.set(7, { status: "offline", lastChecked: "now" }); + + poller.registerViewer(7, "viewer-a", "user-1"); + await flush(); + expect(collectMetrics).not.toHaveBeenCalled(); + + // Core's check comes back: the first sample runs. + fake.hostStatuses.set(7, { status: "reachable", lastChecked: "now" }); + poller.hostStatusChanged(7, "reachable"); + await flush(); + expect(collectMetrics).toHaveBeenCalledOnce(); + }); + + it("reports a failed login and why collection stopped", async () => { + collectMetrics.mockRejectedValue( + new Error("All configured authentication methods failed"), + ); + const { fake, poller } = setup(); + + poller.registerViewer(7, "viewer-a", "user-1"); + await flush(); + + expect(fake.statusReports).toEqual([ + { hostId: 7, ok: false, hostKeyChanged: false }, + ]); + expect(fake.emitted.at(-1)?.payload).toMatchObject({ + state: "auth_failed", + }); + }); + + it("marks a changed host key", async () => { + collectMetrics.mockRejectedValue(new Error("Host key changed")); + const { fake, poller, state } = setup(); + + poller.registerViewer(7, "viewer-a", "user-1"); + await flush(); + + expect(fake.statusReports).toEqual([ + { hostId: 7, ok: false, hostKeyChanged: true }, + ]); + expect(state.authFailures.shouldSkip(7)).toBe(true); + poller.hostKeyAccepted(7); + expect(state.authFailures.shouldSkip(7)).toBe(false); + }); + + it("stops with the last viewer", async () => { + collectMetrics.mockResolvedValue(sample); + const { fake, poller } = setup(); + + poller.registerViewer(7, "a", "user-1"); + poller.registerViewer(7, "b", "user-1"); + await flush(); + const job = fake.scheduled.find((entry) => entry.kind === "every")!; + + poller.unregisterViewer(7, "a"); + expect(job.stopped).toBe(false); + poller.unregisterViewer(7, "b"); + expect(job.stopped).toBe(true); + expect(poller.isPolling(7)).toBe(false); + }); + + it("drops viewers that stop sending heartbeats", async () => { + collectMetrics.mockResolvedValue(sample); + const { poller } = setup(); + poller.registerViewer(7, "a", "user-1"); + await flush(); + + poller.cleanupInactiveViewers(Date.now() + 121_000); + expect(poller.hasViewers(7)).toBe(false); + }); + + it("skips a host with metrics turned off", async () => { + const { fake, poller } = setup(); + await fake.ctx.settings.setHost(7, "metricsEnabled", false); + + poller.registerViewer(7, "a", "user-1"); + await flush(); + + expect(poller.isPolling(7)).toBe(false); + expect(collectMetrics).not.toHaveBeenCalled(); + }); + + it("grows poll concurrency with the number of polled hosts", async () => { + collectMetrics.mockResolvedValue(sample); + const ids = Array.from({ length: 200 }, (_, index) => index + 1); + const { poller, state } = setup(ids); + expect(state.metricsLimiter.limit).toBe(5); + + for (const id of ids) poller.registerViewer(id, `v${id}`, "user-1"); + await flush(); + + expect(state.metricsLimiter.limit).toBe(10); + }); + + it("drops a pooled connection when a host changes", async () => { + collectMetrics.mockResolvedValue(sample); + const { fake, poller } = setup(); + poller.registerViewer(7, "a", "user-1"); + await flush(); + + await poller.hostUpdated(7); + expect(fake.droppedPools).toEqual([{ pool: "stats", hostId: 7 }]); + expect(poller.isPolling(7)).toBe(true); + }); + + it("forgets a deleted host", async () => { + collectMetrics.mockResolvedValue(sample); + const { poller } = setup(); + poller.registerViewer(7, "a", "user-1"); + await flush(); + + poller.hostDeleted(7); + expect(poller.isPolling(7)).toBe(false); + expect(poller.getMetrics(7)).toBeUndefined(); + expect(poller.hasViewers(7)).toBe(false); + }); +}); diff --git a/plugins/host-metrics/tests/backend/repositories/host-metrics-history-repository.test.ts b/plugins/host-metrics/tests/backend/repositories/host-metrics-history-repository.test.ts deleted file mode 100644 index b9441effa..000000000 --- a/plugins/host-metrics/tests/backend/repositories/host-metrics-history-repository.test.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { afterEach, describe, expect, it } from "vitest"; -import { TestSqliteDatabase } from "../../../../../src/backend/tests/database/repositories/test-support.js"; -import { HostMetricsHistoryRepository } from "../../../../../src/backend/database/repositories/host-metrics-history-repository.js"; - -describe("HostMetricsHistoryRepository", () => { - let adapter: TestSqliteDatabase | null = null; - - afterEach(async () => { - if (adapter) { - await adapter.close(); - adapter = null; - } - }); - - async function createRepository( - onWrite?: () => void | Promise, - ): Promise { - adapter = new TestSqliteDatabase(); - const context = await adapter.connect(); - await adapter.exec(` - INSERT INTO users (id, username, password_hash) VALUES - ('user-1', 'user-1', 'hash'), - ('user-2', 'user-2', 'hash'); - - INSERT INTO ssh_data (id, user_id, name, ip, port, username, auth_type) - VALUES (1, 'user-1', 'one', '10.0.0.1', 22, 'root', 'password'), (2, 'user-2', 'two', '10.0.0.1', 22, 'root', 'password'); - INSERT INTO host_metrics_history ( - host_id, ts, cpu_percent, mem_percent, disk_percent, net_rx_bytes, net_tx_bytes - ) - VALUES - (1, '2026-01-01 00:00:00', 10, 20, 30, 100, 200), - (1, '2026-01-02 00:00:00', 11, 21, 31, 101, 201), - (1, '2999-01-01 00:00:00', 12, 22, 32, 102, 202), - (2, '2026-01-02 00:00:00', 99, 99, 99, 999, 999); - `); - - return new HostMetricsHistoryRepository(context, onWrite); - } - - it("creates and lists metrics history rows by range", async () => { - let writeCount = 0; - const repo = await createRepository(() => { - writeCount += 1; - }); - - await repo.create({ - hostId: 1, - cpuPercent: 12, - memPercent: 22, - diskPercent: 32, - netRxBytes: 102, - netTxBytes: 202, - }); - - const rows = await repo.listRange( - 1, - "2026-01-01 00:00:00", - "2026-01-02 23:59:59", - ); - - expect(rows.map((row) => row.cpuPercent)).toEqual([10, 11]); - expect(writeCount).toBe(1); - }); - - it("prunes old history for a host only", async () => { - const repo = await createRepository(); - - await repo.pruneOlderThan(1, 1); - - const rows = await repo.listRange( - 1, - "2000-01-01 00:00:00", - "2999-12-31 23:59:59", - ); - expect(rows.map((row) => row.ts)).toEqual(["2999-01-01 00:00:00"]); - expect( - await repo.listRange(2, "2026-01-01 00:00:00", "2026-01-03 00:00:00"), - ).toHaveLength(1); - }); -}); diff --git a/plugins/host-metrics/tests/backend/repositories/host-metrics-preference-repository.test.ts b/plugins/host-metrics/tests/backend/repositories/host-metrics-preference-repository.test.ts deleted file mode 100644 index e8c2b4476..000000000 --- a/plugins/host-metrics/tests/backend/repositories/host-metrics-preference-repository.test.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { afterEach, describe, expect, it } from "vitest"; -import { TestSqliteDatabase } from "../../../../../src/backend/tests/database/repositories/test-support.js"; -import { HostMetricsPreferenceRepository } from "../../../../../src/backend/database/repositories/host-metrics-preference-repository.js"; - -describe("HostMetricsPreferenceRepository", () => { - let adapter: TestSqliteDatabase | null = null; - - afterEach(async () => { - if (adapter) { - await adapter.close(); - adapter = null; - } - }); - - async function createRepository( - onWrite?: () => void | Promise, - ): Promise { - 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, stats_config, ip, port, username, auth_type) - VALUES (1, 'user-1', 'one', '{}', '10.0.0.1', 22, 'root', 'password'), (2, 'user-2', 'two', '{}', '10.0.0.1', 22, 'root', 'password'); - INSERT INTO host_metrics_preferences ( - user_id, host_id, layout, created_at, updated_at - ) - VALUES ( - 'user-1', - 1, - '{"slots":[],"columns":3}', - '2026-01-01T00:00:00.000Z', - '2026-01-01T00:00:00.000Z' - ); - `); - - return new HostMetricsPreferenceRepository(context, onWrite); - } - - it("finds a saved layout by user and host", async () => { - const repo = await createRepository(); - - const existing = await repo.findByUserAndHost("user-1", 1); - expect(existing?.layout).toBe('{"slots":[],"columns":3}'); - expect(await repo.findByUserAndHost("user-1", 2)).toBeNull(); - }); - - it("updates and inserts layouts with write notifications", async () => { - let writeCount = 0; - const repo = await createRepository(() => { - writeCount += 1; - }); - - const updated = await repo.upsertLayout( - "user-1", - 1, - '{"slots":[{"id":"cpu"}],"columns":2}', - "2026-02-01T00:00:00.000Z", - ); - expect(updated).toMatchObject({ - id: 1, - layout: '{"slots":[{"id":"cpu"}],"columns":2}', - updatedAt: "2026-02-01T00:00:00.000Z", - }); - - const created = await repo.upsertLayout( - "user-2", - 2, - '{"slots":[{"id":"mem"}],"columns":1}', - "2026-03-01T00:00:00.000Z", - ); - expect(created).toMatchObject({ - userId: "user-2", - hostId: 2, - layout: '{"slots":[{"id":"mem"}],"columns":1}', - createdAt: "2026-03-01T00:00:00.000Z", - updatedAt: "2026-03-01T00:00:00.000Z", - }); - expect(writeCount).toBe(2); - }); - - it("updates host stats config for the owning user", async () => { - let writeCount = 0; - const repo = await createRepository(() => { - writeCount += 1; - }); - - await expect( - repo.updateHostStatsConfig( - "user-1", - 1, - '{"enabledWidgets":["cpu","memory"]}', - ), - ).resolves.toBe(true); - await expect( - repo.updateHostStatsConfig("user-2", 1, '{"enabledWidgets":["disk"]}'), - ).resolves.toBe(false); - - expect(writeCount).toBe(1); - }); - - it("deletes all metric preferences for a user", async () => { - let writeCount = 0; - const repo = await createRepository(() => { - writeCount += 1; - }); - - await repo.upsertLayout("user-2", 2, '{"slots":["mem"]}'); - - await expect(repo.deleteByUserId("user-1")).resolves.toBe(1); - await expect(repo.deleteByUserId("missing")).resolves.toBe(0); - - expect(await repo.findByUserAndHost("user-1", 1)).toBeNull(); - expect((await repo.findByUserAndHost("user-2", 2))?.layout).toBe( - '{"slots":["mem"]}', - ); - expect(writeCount).toBe(2); - }); -}); diff --git a/plugins/host-metrics/tests/backend/routes.test.ts b/plugins/host-metrics/tests/backend/routes.test.ts new file mode 100644 index 000000000..3b66c7485 --- /dev/null +++ b/plugins/host-metrics/tests/backend/routes.test.ts @@ -0,0 +1,242 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { startServer, type TestServer } from "./server"; + +let server: TestServer | null = null; + +afterEach(async () => { + await server?.close(); + server = null; +}); + +const ROUTES: Array<[string, string, unknown?]> = [ + ["GET", "/defaults"], + ["GET", "/metrics/7"], + ["POST", "/metrics/start/7"], + ["POST", "/metrics/stop/7", {}], + ["POST", "/metrics/connect-totp", { sessionId: "x", totpCode: "1" }], + ["POST", "/metrics/heartbeat", { viewerSessionId: "x" }], + ["POST", "/metrics/register-viewer", { hostId: 7 }], + ["POST", "/metrics/unregister-viewer", { hostId: 7, viewerSessionId: "x" }], + ["GET", "/metrics/history/7"], + ["GET", "/host-metrics/preferences/7"], + ["POST", "/host-metrics/preferences/7", { slots: [], columns: 3 }], + ["GET", "/host-metrics/platform/7"], + ["POST", "/host-metrics/managers/health/7/config", { checks: [] }], +]; + +describe("permissions", () => { + it.each(ROUTES)( + "%s %s answers 403 without host-metrics.use", + async (method, path, body) => { + server = await startServer({ permissions: [] }); + const response = await server.request(method, path, { body }); + expect(response.status).toBe(403); + }, + ); +}); + +describe("defaults", () => { + it("tells any user whether new hosts start with metrics on", async () => { + server = await startServer(); + expect((await server.request("GET", "/defaults")).body).toEqual({ + enabledForNewHosts: true, + }); + await server.mock.ctx.settings.set("enabledForNewHosts", false); + expect((await server.request("GET", "/defaults")).body).toEqual({ + enabledForNewHosts: false, + }); + }); +}); + +describe("metrics", () => { + it("answers 404 until a sample exists", async () => { + server = await startServer(); + const response = await server.request("GET", "/metrics/7"); + expect(response.status).toBe(404); + expect(response.body.error).toBe("Metrics not available"); + }); + + it("refuses a host the user cannot see", async () => { + server = await startServer(); + const response = await server.request("GET", "/metrics/99"); + expect(response.status).toBe(403); + }); + + it("rejects a bad host id", async () => { + server = await startServer(); + expect((await server.request("GET", "/metrics/abc")).status).toBe(400); + }); +}); + +describe("viewers", () => { + it("registers, heartbeats and unregisters a viewer", async () => { + server = await startServer(); + const registered = await server.request( + "POST", + "/metrics/register-viewer", + { + body: { hostId: 7 }, + }, + ); + expect(registered.status).toBe(200); + const viewerSessionId = registered.body.viewerSessionId as string; + expect(viewerSessionId).toMatch(/^viewer-/); + + expect( + ( + await server.request("POST", "/metrics/heartbeat", { + body: { viewerSessionId }, + }) + ).status, + ).toBe(200); + // Another user cannot keep or drop someone else's viewer. + expect( + ( + await server.request("POST", "/metrics/heartbeat", { + user: "user-2", + body: { viewerSessionId }, + }) + ).status, + ).toBe(404); + + await server.request("POST", "/metrics/unregister-viewer", { + body: { hostId: 7, viewerSessionId }, + }); + expect( + ( + await server.request("POST", "/metrics/heartbeat", { + body: { viewerSessionId }, + }) + ).status, + ).toBe(404); + }); + + it("skips a host with metrics turned off", async () => { + server = await startServer(); + await server.mock.ctx.settings.setHost(7, "metricsEnabled", false); + const response = await server.request("POST", "/metrics/register-viewer", { + body: { hostId: 7 }, + }); + expect(response.body).toEqual({ + success: true, + skipped: true, + reason: "metrics_disabled", + }); + }); + + it("skips a host it cannot find", async () => { + server = await startServer(); + const response = await server.request("POST", "/metrics/register-viewer", { + body: { hostId: 99 }, + }); + expect(response.body.reason).toBe("host_not_found"); + }); +}); + +describe("history", () => { + it("returns stored samples in the range", async () => { + server = await startServer(); + const now = new Date(); + const ts = now.toISOString().slice(0, 19).replace("T", " "); + server.db.sqlite + .prepare( + "INSERT INTO p_host_metrics_host_metrics_history (host_id, ts, cpu_percent) VALUES (?, ?, ?)", + ) + .run(7, ts, 42); + + const response = await server.request("GET", "/metrics/history/7?range=1h"); + expect(response.status).toBe(200); + expect(response.body.rows).toEqual([ + expect.objectContaining({ ts, cpu_percent: 42 }), + ]); + }); + + it("rejects an unknown range", async () => { + server = await startServer(); + const response = await server.request("GET", "/metrics/history/7?range=2y"); + expect(response.status).toBe(400); + }); +}); + +describe("layout preferences", () => { + it("defaults from the host's enabled widgets", async () => { + server = await startServer(); + await server.mock.ctx.settings.setHost(7, "enabledWidgets", ["cpu"]); + const response = await server.request("GET", "/host-metrics/preferences/7"); + expect(response.body.layout.slots.map((s: { id: string }) => s.id)).toEqual( + ["cpu"], + ); + }); + + it("saves a layout and keeps the owner's enabled widgets in step", async () => { + server = await startServer(); + const layout = { + columns: 2, + slots: [ + { id: "memory", order: 0, colSpan: 1, height: null }, + { id: "service_manager", order: 1, colSpan: 2, height: null }, + ], + }; + expect( + ( + await server.request("POST", "/host-metrics/preferences/7", { + body: layout, + }) + ).status, + ).toBe(200); + + const read = await server.request("GET", "/host-metrics/preferences/7"); + expect(read.body.layout).toEqual(layout); + expect(await server.mock.ctx.settings.getHost(7, "enabledWidgets")).toEqual( + ["memory"], + ); + }); + + it("rejects a malformed layout", async () => { + server = await startServer(); + const response = await server.request( + "POST", + "/host-metrics/preferences/7", + { body: { columns: 2 } }, + ); + expect(response.status).toBe(400); + }); +}); + +describe("health checks", () => { + it("saves a user's checks for a host", async () => { + server = await startServer(); + const checks = [ + { id: "web", name: "Web", type: "tcp", target: "x", port: 80 }, + ]; + const response = await server.request( + "POST", + "/host-metrics/managers/health/7/config", + { body: { checks, intervalSeconds: 120 } }, + ); + expect(response.status).toBe(200); + expect( + server.db.sqlite + .prepare( + "SELECT user_id, checks, interval_seconds FROM p_host_metrics_host_health_checks", + ) + .all(), + ).toEqual([ + { + user_id: "user-1", + checks: JSON.stringify(checks), + interval_seconds: 120, + }, + ]); + }); + + it("rejects invalid checks", async () => { + server = await startServer(); + const response = await server.request( + "POST", + "/host-metrics/managers/health/7/config", + { body: { checks: [{ id: "x" }] } }, + ); + expect(response.status).toBe(400); + }); +}); diff --git a/plugins/host-metrics/tests/backend/server.ts b/plugins/host-metrics/tests/backend/server.ts new file mode 100644 index 000000000..a963be12f --- /dev/null +++ b/plugins/host-metrics/tests/backend/server.ts @@ -0,0 +1,132 @@ +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 MockContextOptions, + 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; +} + +/** A host owned by user-1, as ctx.hosts and ctx.ssh.resolveHost see it. */ +export function testHost(id: number, extra: Record = {}) { + return { + summary: { + id, + userId: "user-1", + name: `host-${id}`, + ip: `10.0.0.${id}`, + port: 22, + username: "root", + tags: null, + folder: null, + authType: "password", + }, + ssh: { + id, + userId: "user-1", + ip: `10.0.0.${id}`, + port: 22, + username: "root", + authType: "password", + connectionType: "ssh", + enableSsh: true, + password: "pw", + ...extra, + }, + }; +} + +/** + * The plugin activated against a real in-memory database, its router mounted + * the way core mounts it: JSON parsed and the acting user set per request. + */ +export async function startServer( + options: { + permissions?: string[]; + hosts?: number[]; + mock?: Partial; + } = {}, +): Promise { + 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); + } + const hosts = (options.hosts ?? [7]).map((id) => testHost(id)); + for (const host of hosts) { + db.sqlite.prepare("INSERT INTO ssh_data (id) VALUES (?)").run(host.ssh.id); + } + + let router: Router | null = null; + const mock = createMockCtx({ + pluginId: manifest.id, + manifest, + capabilities: manifest.capabilities, + db: db.database, + router: () => (router = express.Router()), + permissions: options.permissions ?? ["host-metrics.use"], + hosts: hosts.map((host) => host.summary), + sshHosts: hosts.map((host) => host.ssh), + ...options.mock, + }); + + const { activate } = await import("../../src/backend/index.js"); + await activate(mock.ctx); + + const app = express(); + app.use(express.json()); + app.use((req, _res, next) => { + mock.setActor(req.header("x-test-user") ?? undefined); + next(); + }); + app.use((req, res, next) => router!(req, res, next)); + + const server = http.createServer(app); + await new Promise((resolve) => server.listen(0, resolve)); + const { port } = server.address() as AddressInfo; + + return { + db, + mock, + async request(method, path, { user = "user-1", body: given } = {}) { + const body = method === "GET" ? undefined : given; + const response = await fetch(`http://127.0.0.1:${port}${path}`, { + method, + headers: { + "x-test-user": user, + ...(body !== undefined ? { "content-type": "application/json" } : {}), + }, + body: body !== undefined ? JSON.stringify(body) : undefined, + }); + const text = await response.text(); + return { status: response.status, body: text ? JSON.parse(text) : null }; + }, + async close() { + for (const dispose of mock.disposals.reverse()) await dispose(); + await new Promise((resolve) => server.close(() => resolve())); + db.close(); + }, + }; +} diff --git a/plugins/host-metrics/tests/backend/metrics-shared/state.test.ts b/plugins/host-metrics/tests/backend/state.test.ts similarity index 92% rename from plugins/host-metrics/tests/backend/metrics-shared/state.test.ts rename to plugins/host-metrics/tests/backend/state.test.ts index 17b92bb0f..e5c157f30 100644 --- a/plugins/host-metrics/tests/backend/metrics-shared/state.test.ts +++ b/plugins/host-metrics/tests/backend/state.test.ts @@ -1,11 +1,14 @@ import { describe, expect, it, vi } from "vitest"; import { - authFailureTracker, + AuthFailureTracker, canStartInitialMetrics, ConcurrentLimiter, + createMetricsState, HostPollCache, metricsConcurrencyFor, -} from "../../../../../src/backend/hosts/metrics-shared/state.js"; +} from "../../src/backend/state.js"; + +const authFailureTracker = new AuthFailureTracker(); describe("auth failure tracking", () => { it("permanently pauses polling after a host key mismatch", () => { @@ -22,13 +25,23 @@ describe("auth failure tracking", () => { }); describe("initial metrics admission", () => { - it("requires both an active viewer and a confirmed online status", () => { + it("needs a viewer and a host core does not see as offline", () => { expect(canStartInitialMetrics("online", true)).toBe(true); expect(canStartInitialMetrics("reachable", true)).toBe(true); expect(canStartInitialMetrics("offline", true)).toBe(false); - expect(canStartInitialMetrics(undefined, true)).toBe(false); expect(canStartInitialMetrics("online", false)).toBe(false); - expect(canStartInitialMetrics(undefined, true, false)).toBe(true); + // Status checks off: core has no status, so the sample is tried. + expect(canStartInitialMetrics(null, true)).toBe(true); + }); +}); + +describe("createMetricsState", () => { + it("gives every activation its own trackers", () => { + const first = createMetricsState(); + const second = createMetricsState(); + first.authFailures.recordFailure(1, "AUTH", true); + expect(first.authFailures.shouldSkip(1)).toBe(true); + expect(second.authFailures.shouldSkip(1)).toBe(false); }); }); diff --git a/plugins/host-metrics/tests/backend/status-poll-auth.test.ts b/plugins/host-metrics/tests/backend/status-poll-auth.test.ts deleted file mode 100644 index 85e5ed667..000000000 --- a/plugins/host-metrics/tests/backend/status-poll-auth.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { describe, it, expect } from "vitest"; -import fs from "fs"; -import path from "path"; - -// Regression guard for Support#1216: routine status polling on a host with -// metrics disabled ran a full SSH authentication attempt on every interval, -// which fires a live 2FA push each cycle on RADIUS/Duo-backed devices with -// no active session. Spinning up the full metrics-service Express app (DB, -// SSH clients, timers, polling managers, etc.) just to drive pollHostStatus -// through two intervals is out of scope, so this asserts directly against -// the source that the status-poll path never calls into SSH auth, the same -// way login-alert-route-order.test.ts guards route registration order. -describe("metrics service status polling", () => { - it("never authenticates over SSH from pollHostStatus, on the first poll or any later one", () => { - const source = fs.readFileSync( - path.resolve(__dirname, "../../src/backend/routes.ts"), - "utf8", - ); - - const pollHostStatusStart = source.indexOf("private async pollHostStatus("); - const pollHostMetricsStart = source.indexOf( - "private async pollHostMetrics(", - ); - - expect(pollHostStatusStart).toBeGreaterThan(-1); - expect(pollHostMetricsStart).toBeGreaterThan(pollHostStatusStart); - - const pollHostStatusBody = source.slice( - pollHostStatusStart, - pollHostMetricsStart, - ); - - expect(pollHostStatusBody).not.toContain("withSshConnection"); - expect(pollHostStatusBody).not.toContain("statusAfterAuthentication"); - }); -}); diff --git a/plugins/host-metrics/tests/frontend/HostEditorStatsTab.test.tsx b/plugins/host-metrics/tests/frontend/HostEditorStatsTab.test.tsx new file mode 100644 index 000000000..59fa6b779 --- /dev/null +++ b/plugins/host-metrics/tests/frontend/HostEditorStatsTab.test.tsx @@ -0,0 +1,49 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { cleanup, fireEvent, render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom/vitest"; +import { HostStatsTab } from "../../src/frontend/HostEditorStatsTab"; + +afterEach(cleanup); + +function renderSection(pluginSettings: Record = {}) { + let form: Record = { + quickActions: [], + pluginSettings: { "host-metrics": pluginSettings, other: { keep: 1 } }, + }; + const updateForm = vi.fn( + (patch: (current: Record) => Record) => { + form = { ...form, ...patch(form) }; + }, + ); + render( + , + ); + return { updateForm, form: () => form }; +} + +describe("HostStatsTab", () => { + it("writes the plugin's host settings and leaves other plugins alone", () => { + const { form } = renderSection({ metricsEnabled: true }); + fireEvent.change( + screen.getByPlaceholderText("hosts.excludedMountsPlaceholder"), + { target: { value: "/snap" } }, + ); + fireEvent.click(screen.getByText(/hosts.addExcludedMount/)); + expect(form().pluginSettings).toEqual({ + "host-metrics": { metricsEnabled: true, excludedMounts: ["/snap"] }, + other: { keep: 1 }, + }); + }); + + it("has no status check options, which are core's", () => { + renderSection(); + expect(screen.queryByText("hosts.statusChecksLabel")).toBeNull(); + }); +}); diff --git a/plugins/host-metrics/tests/frontend/HostMetricsTab.test.tsx b/plugins/host-metrics/tests/frontend/HostMetricsTab.test.tsx index 1d9056536..7054dc448 100644 --- a/plugins/host-metrics/tests/frontend/HostMetricsTab.test.tsx +++ b/plugins/host-metrics/tests/frontend/HostMetricsTab.test.tsx @@ -1,7 +1,7 @@ import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; import { cleanup, render, screen } from "@testing-library/react"; import "@testing-library/jest-dom/vitest"; -import type { ServerMetrics } from "@/main-axios"; +import type { ServerMetrics } from "../../src/shared/metrics.js"; vi.mock("react-i18next", () => ({ useTranslation: () => ({ t: (key: string) => key }), @@ -35,25 +35,28 @@ const SAMPLE = { lastChecked: "2026-09-18T07:00:00.000Z", } as ServerMetrics; -vi.mock("@/main-axios.ts", async (importOriginal) => ({ - ...(await importOriginal()), - getServerStatusById: vi.fn(async () => ({ status: "online" })), - getServerMetricsById: vi.fn(async () => SAMPLE), - startMetricsPolling: vi.fn(async () => ({ viewerSessionId: "viewer-1" })), - stopMetricsPolling: vi.fn(async () => undefined), - sendMetricsHeartbeat: vi.fn(async () => true), - getSnippets: vi.fn(async () => []), - getSSHHosts: vi.fn(async () => []), - logActivity: vi.fn(async () => undefined), -})); +vi.mock("../../src/frontend/host-metrics-api", async (importOriginal) => { + const api = { + getMetrics: vi.fn(async () => SAMPLE), + startMetrics: vi.fn(async () => ({ + success: true, + viewerSessionId: "viewer-1", + })), + stopMetrics: vi.fn(async () => undefined), + heartbeat: vi.fn(async () => true), + submitTotp: vi.fn(), + }; + return { + ...(await importOriginal()), + useHostMetricsApi: () => api, + }; +}); -vi.mock("@/contexts/UiPreferencesContext", async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock("@termix/plugin-sdk/ui", async (importOriginal) => ({ + ...(await importOriginal()), useAreaPreferences: () => ({ columns: 3 }), -})); - -vi.mock("@/hooks/use-confirmation.ts", () => ({ useConfirmation: () => ({ confirmWithToast: vi.fn() }), + logActivity: vi.fn(async () => undefined), })); vi.mock("../../src/frontend/hooks/useHostMetricsPreferences.ts", () => ({ @@ -90,11 +93,13 @@ describe("HostMetricsTab", () => { username: "admin", port: 22, authType: "password", - statsConfig: JSON.stringify({ - metricsEnabled: true, - metricsInterval: 30, - enabledWidgets: ["gpu"], - }), + pluginSettings: { + "host-metrics": { + metricsEnabled: true, + metricsInterval: 30, + enabledWidgets: ["gpu"], + }, + }, }} />, ); diff --git a/plugins/host-metrics/tests/frontend/activate.test.tsx b/plugins/host-metrics/tests/frontend/activate.test.tsx index 814097280..3fe29be30 100644 --- a/plugins/host-metrics/tests/frontend/activate.test.tsx +++ b/plugins/host-metrics/tests/frontend/activate.test.tsx @@ -38,6 +38,18 @@ describe(`${manifest.id} activate`, () => { } }); + it("fills core's metric slots and offers disk usage to other plugins", async () => { + rendered = await renderWithApp(plugin, { manifest, locales }); + expect(rendered.registered.slot("dashboard.hostMetrics")).toEqual([ + "host-metrics.dashboardHost", + ]); + expect(rendered.registered.slot("homepage.hostMetrics")).toEqual([ + "host-metrics.homepageHost", + ]); + expect(rendered.registered.actions()).toContain("host-metrics.disk"); + expect(rendered.registered.hostEditorSections()).toEqual(["host-metrics"]); + }); + it("removes everything it registered on deactivate", async () => { const app = await renderWithApp(plugin, { manifest, locales }); await app.deactivate(); diff --git a/plugins/host-metrics/tests/frontend/cards/GpuCard.test.tsx b/plugins/host-metrics/tests/frontend/cards/GpuCard.test.tsx index 8ba0c3caa..a3ef9b82a 100644 --- a/plugins/host-metrics/tests/frontend/cards/GpuCard.test.tsx +++ b/plugins/host-metrics/tests/frontend/cards/GpuCard.test.tsx @@ -1,11 +1,12 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { cleanup, render, screen } from "@testing-library/react"; import "@testing-library/jest-dom/vitest"; -import type { ServerMetrics } from "@/main-axios"; -import type { GpuMetrics } from "@/types/stats-widgets"; +import type { ServerMetrics } from "../../../src/shared/metrics.js"; +import type { GpuMetrics } from "../../../src/shared/stats-widgets.js"; vi.mock("react-i18next", () => ({ useTranslation: () => ({ t: (key: string) => key }), + initReactI18next: { type: "3rdParty", init: () => {} }, })); import { GpuCard } from "../../../src/frontend/cards/GpuCard"; diff --git a/plugins/host-metrics/tests/frontend/cards/gpu-history.test.ts b/plugins/host-metrics/tests/frontend/cards/gpu-history.test.ts index b17991374..0df8dc22a 100644 --- a/plugins/host-metrics/tests/frontend/cards/gpu-history.test.ts +++ b/plugins/host-metrics/tests/frontend/cards/gpu-history.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import type { GpuDevice } from "@/types/stats-widgets"; +import type { GpuDevice } from "../../../src/shared/stats-widgets.js"; import { appendGpuHistories } from "../../../src/frontend/cards/gpu-history"; function gpu( diff --git a/plugins/host-metrics/tests/frontend/host-metrics-api.test.ts b/plugins/host-metrics/tests/frontend/host-metrics-api.test.ts new file mode 100644 index 000000000..a97efd0ac --- /dev/null +++ b/plugins/host-metrics/tests/frontend/host-metrics-api.test.ts @@ -0,0 +1,73 @@ +import { describe, expect, it, vi } from "vitest"; +import { createHostMetricsApi } from "../../src/frontend/host-metrics-api"; + +function client() { + return { + get: vi.fn(), + post: vi.fn(), + put: vi.fn(), + patch: vi.fn(), + delete: vi.fn(), + }; +} + +const notFound = () => + Object.assign(new Error("Not found"), { response: { status: 404 } }); + +describe("host metrics api", () => { + it("answers null while a host has no sample", async () => { + const api = client(); + api.get.mockRejectedValue(notFound()); + await expect(createHostMetricsApi(api).getMetrics(7)).resolves.toBeNull(); + expect(api.get).toHaveBeenCalledWith("/metrics/7"); + }); + + it("reports a swept viewer instead of throwing", async () => { + const api = client(); + api.post.mockRejectedValueOnce(notFound()); + await expect( + createHostMetricsApi(api).heartbeat("expired-viewer"), + ).resolves.toBe(false); + expect(api.post).toHaveBeenCalledWith("/metrics/heartbeat", { + viewerSessionId: "expired-viewer", + }); + + api.post.mockResolvedValueOnce({ data: { success: true } }); + await expect(createHostMetricsApi(api).heartbeat("live")).resolves.toBe( + true, + ); + }); + + it("carries the server's connection logs on a failed start", async () => { + const api = client(); + api.post.mockRejectedValue( + Object.assign(new Error("boom"), { + response: { + status: 500, + data: { error: "No route", connectionLogs: [{ message: "x" }] }, + }, + }), + ); + await expect( + createHostMetricsApi(api).startMetrics(7), + ).rejects.toMatchObject({ + message: "No route", + connectionLogs: [{ message: "x" }], + }); + }); + + it("puts the host id between a manager and its action", async () => { + const api = client(); + api.post.mockResolvedValue({ data: { success: true } }); + await createHostMetricsApi(api).managerPost( + 7, + "services", + { a: 1 }, + "action", + ); + expect(api.post).toHaveBeenCalledWith( + "/host-metrics/managers/services/7/action", + { a: 1 }, + ); + }); +}); diff --git a/plugins/host-metrics/tests/frontend/host-metrics-status-api.test.ts b/plugins/host-metrics/tests/frontend/host-metrics-status-api.test.ts deleted file mode 100644 index 4c407dc0f..000000000 --- a/plugins/host-metrics/tests/frontend/host-metrics-status-api.test.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; - -const statsApiMock = vi.hoisted(() => ({ get: vi.fn(), post: vi.fn() })); -const remoteStatsApiMock = vi.hoisted(() => ({ get: vi.fn() })); -const sshHostApiMock = vi.hoisted(() => ({ get: vi.fn() })); -const resolveConnectionOriginMock = vi.hoisted(() => vi.fn()); - -vi.mock("@/main-axios", () => ({ - statsApi: statsApiMock, - sshHostApi: sshHostApiMock, - getRemoteStatsApi: () => remoteStatsApiMock, - isElectron: () => true, - handleApiError: vi.fn(), -})); - -vi.mock("@/lib/hosts-request-cache", () => ({ - getCachedServerStatuses: (loader: () => Promise) => loader(), -})); - -vi.mock("@/lib/connection-origin", () => ({ - resolveConnectionOrigin: resolveConnectionOriginMock, -})); - -import { - getAllServerStatuses, - getServerMetricsById, - sendMetricsHeartbeat, -} from "../../../../src/ui/api/host-metrics-status-api"; - -beforeEach(() => { - vi.clearAllMocks(); - window.electronAPI = { - invoke: vi.fn(async (channel: string) => - channel === "get-remote-sync-config" - ? { serverUrl: "https://termix.example.test" } - : null, - ), - } as unknown as NonNullable; - statsApiMock.get.mockResolvedValue({ data: { 1: { status: "online" } } }); - remoteStatsApiMock.get.mockResolvedValue({ - data: { 2: { status: "reachable" } }, - }); -}); - -describe("status check origin routing", () => { - it("only asks the embedded backend to poll local-origin hosts", async () => { - sshHostApiMock.get.mockResolvedValue({ - data: [ - { id: 1, connectionOrigin: "local" }, - { id: 2, connectionOrigin: "remote" }, - ], - }); - resolveConnectionOriginMock.mockImplementation(async (host) => - host.connectionOrigin === "local" ? "local" : "remote", - ); - - await expect(getAllServerStatuses()).resolves.toEqual({ - 1: { status: "online" }, - 2: { status: "reachable" }, - }); - - expect(statsApiMock.get).toHaveBeenCalledWith("/status", { - timeout: 2000, - params: { hostIds: "1" }, - __silentRetry: true, - }); - }); - - it("sends an empty allowlist when every host uses the remote server", async () => { - sshHostApiMock.get.mockResolvedValue({ - data: [{ id: 2, connectionOrigin: "remote" }], - }); - resolveConnectionOriginMock.mockResolvedValue("remote"); - - await getAllServerStatuses(); - - expect(statsApiMock.get).toHaveBeenCalledWith("/status", { - timeout: 2000, - params: { hostIds: "" }, - __silentRetry: true, - }); - }); -}); - -describe("metrics request coalescing", () => { - it("shares simultaneous reads for the same host", async () => { - statsApiMock.get.mockResolvedValueOnce({ - status: 200, - data: { cpu: { percent: 12 } }, - }); - - const [first, second] = await Promise.all([ - getServerMetricsById(991), - getServerMetricsById(991), - ]); - - expect(first).toEqual({ cpu: { percent: 12 } }); - expect(second).toBe(first); - expect(statsApiMock.get).toHaveBeenCalledOnce(); - }); -}); - -describe("metrics viewer heartbeat", () => { - it("reports a swept viewer session without throwing or logging", async () => { - statsApiMock.post.mockResolvedValueOnce({ status: 404 }); - - await expect(sendMetricsHeartbeat("expired-viewer")).resolves.toBe(false); - expect(statsApiMock.post).toHaveBeenCalledWith( - "/metrics/heartbeat", - { viewerSessionId: "expired-viewer" }, - { validateStatus: expect.any(Function) }, - ); - }); - - it("keeps a live viewer session", async () => { - statsApiMock.post.mockResolvedValueOnce({ status: 200 }); - await expect(sendMetricsHeartbeat("live-viewer")).resolves.toBe(true); - }); -}); diff --git a/plugins/host-metrics/tests/frontend/metrics-change-key.test.ts b/plugins/host-metrics/tests/frontend/metrics-change-key.test.ts index c17032be3..6fb71d92f 100644 --- a/plugins/host-metrics/tests/frontend/metrics-change-key.test.ts +++ b/plugins/host-metrics/tests/frontend/metrics-change-key.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import type { ServerMetrics } from "@/main-axios"; -import type { GpuMetrics } from "@/types/stats-widgets"; +import type { ServerMetrics } from "../../src/shared/metrics.js"; +import type { GpuMetrics } from "../../src/shared/stats-widgets.js"; import { metricsChangeKey } from "../../src/frontend/metrics-change-key"; function poll(gpu: GpuMetrics): ServerMetrics { diff --git a/plugins/host-metrics/tests/frontend/summary-store.test.ts b/plugins/host-metrics/tests/frontend/summary-store.test.ts new file mode 100644 index 000000000..5a7d1a4e1 --- /dev/null +++ b/plugins/host-metrics/tests/frontend/summary-store.test.ts @@ -0,0 +1,56 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { createMetricsSummaryStore } from "../../src/frontend/summary-store"; +import type { HostMetricsApi } from "../../src/frontend/host-metrics-api"; + +afterEach(() => { + vi.useRealTimers(); +}); + +function fakeApi() { + return { + registerViewer: vi.fn(async () => ({ + success: true, + viewerSessionId: "v1", + })), + unregisterViewer: vi.fn(async () => undefined), + heartbeat: vi.fn(async () => true), + getMetrics: vi.fn(async () => ({ cpu: { percent: 5 } })), + }; +} + +const flush = () => new Promise((resolve) => setTimeout(resolve, 0)); + +describe("metrics summary store", () => { + it("holds one viewer per host while anything watches it", async () => { + const api = fakeApi(); + const store = createMetricsSummaryStore(api as unknown as HostMetricsApi); + const listener = vi.fn(); + + const first = store.watch(7, listener); + const second = store.watch(7, vi.fn()); + await flush(); + + expect(api.registerViewer).toHaveBeenCalledOnce(); + expect(store.get(7)).toEqual({ cpu: { percent: 5 } }); + expect(listener).toHaveBeenCalled(); + + first(); + expect(api.unregisterViewer).not.toHaveBeenCalled(); + second(); + expect(api.unregisterViewer).toHaveBeenCalledWith(7, "v1"); + expect(store.get(7)).toBeNull(); + }); + + it("registers again when the server dropped the viewer", async () => { + vi.useFakeTimers(); + const api = fakeApi(); + const store = createMetricsSummaryStore(api as unknown as HostMetricsApi); + store.watch(7, vi.fn()); + await vi.advanceTimersByTimeAsync(0); + + api.heartbeat.mockResolvedValueOnce(false); + await vi.advanceTimersByTimeAsync(30_000); + expect(api.registerViewer).toHaveBeenCalledTimes(2); + store.dispose(); + }); +}); diff --git a/plugins/host-metrics/tsconfig.json b/plugins/host-metrics/tsconfig.json index 1d3e42542..7b35314c3 100644 --- a/plugins/host-metrics/tsconfig.json +++ b/plugins/host-metrics/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/proxmox/src/backend/routes.ts b/plugins/proxmox/src/backend/routes.ts index 308861d86..07fb13d78 100644 --- a/plugins/proxmox/src/backend/routes.ts +++ b/plugins/proxmox/src/backend/routes.ts @@ -1,7 +1,7 @@ import { getErrorMessage } from "../../../../src/backend/utils/error-message.js"; import express, { type Router } from "express"; import type { Client as SSHClient } from "ssh2"; -import { execElevated } from "../../../../src/backend/hosts/metrics-shared/exec-elevated.js"; +import { execElevated } from "@termix/plugin-sdk/host-commands"; import type { PluginHostCreateInput, PluginHostRecord, diff --git a/plugins/proxmox/tests/backend/proxmox-sudo.test.ts b/plugins/proxmox/tests/backend/proxmox-sudo.test.ts index 5a4d44694..1c49c02a9 100644 --- a/plugins/proxmox/tests/backend/proxmox-sudo.test.ts +++ b/plugins/proxmox/tests/backend/proxmox-sudo.test.ts @@ -2,12 +2,10 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { Client } from "ssh2"; const execElevated = vi.fn(); -vi.mock( - "../../../../src/backend/hosts/metrics-shared/exec-elevated.js", - () => ({ - execElevated: (...args: unknown[]) => execElevated(...args), - }), -); +vi.mock("@termix/plugin-sdk/host-commands", async (importOriginal) => ({ + ...(await importOriginal()), + execElevated: (...args: unknown[]) => execElevated(...args), +})); const { execPveshCommand } = await import("../../src/backend/routes.js"); diff --git a/plugins/remote-desktop/src/backend/index.ts b/plugins/remote-desktop/src/backend/index.ts index ab62cd09c..7c8268eae 100644 --- a/plugins/remote-desktop/src/backend/index.ts +++ b/plugins/remote-desktop/src/backend/index.ts @@ -8,7 +8,11 @@ import { import { resolveGuacdOptions } from "./guacd-config.js"; import { registerRoutes } from "./routes.js"; import { createLiveSessions } from "./live-sessions.js"; -import { normalizeImportedHost } from "./host-settings.js"; +import { + normalizeImportedHost, + PORT_KEY, + readHostSettings, +} from "./host-settings.js"; import { createLogger } from "./log.js"; export async function activate(ctx: PluginContext) { @@ -103,6 +107,14 @@ export async function activate(ctx: PluginContext) { "remote-desktop.hostImportNormalizer", normalizeImportedHost, ); + + // Core's status dot pings these hosts on the protocol's own port. + for (const protocol of ["rdp", "vnc", "telnet"] as const) { + ctx.hosts.status.registerPort(protocol, async (hostId) => { + const settings = await readHostSettings(ctx, hostId); + return settings[PORT_KEY[protocol]] as number; + }); + } } export async function deactivate() {} diff --git a/plugins/remote-desktop/src/frontend/HostEditorGuacamoleTabs.tsx b/plugins/remote-desktop/src/frontend/HostEditorGuacamoleTabs.tsx index 79fd50a48..408d10386 100644 --- a/plugins/remote-desktop/src/frontend/HostEditorGuacamoleTabs.tsx +++ b/plugins/remote-desktop/src/frontend/HostEditorGuacamoleTabs.tsx @@ -82,62 +82,6 @@ export function HostEditorRdpTab({ - } - > -
- - - setField("statsConfig", { - ...form.statsConfig, - statusCheckEnabled: v, - }) - } - /> - - {form.statsConfig.statusCheckEnabled && ( - - - setField("statsConfig", { - ...form.statsConfig, - useGlobalStatusInterval: v, - }) - } - /> - - )} - {form.statsConfig.statusCheckEnabled && - !form.statsConfig.useGlobalStatusInterval && ( - - - setField("statsConfig", { - ...form.statsConfig, - statusCheckInterval: Number(e.target.value), - }) - } - className="w-20 h-7 text-xs text-right [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" - /> - - )} -
-
} @@ -1017,62 +961,6 @@ export function HostEditorVncTab({ - } - > -
- - - setField("statsConfig", { - ...form.statsConfig, - statusCheckEnabled: v, - }) - } - /> - - {form.statsConfig.statusCheckEnabled && ( - - - setField("statsConfig", { - ...form.statsConfig, - useGlobalStatusInterval: v, - }) - } - /> - - )} - {form.statsConfig.statusCheckEnabled && - !form.statsConfig.useGlobalStatusInterval && ( - - - setField("statsConfig", { - ...form.statsConfig, - statusCheckInterval: Number(e.target.value), - }) - } - className="w-20 h-7 text-xs text-right [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" - /> - - )} -
-
} @@ -1555,62 +1443,6 @@ export function HostEditorTelnetTab({ - } - > -
- - - setField("statsConfig", { - ...form.statsConfig, - statusCheckEnabled: v, - }) - } - /> - - {form.statsConfig.statusCheckEnabled && ( - - - setField("statsConfig", { - ...form.statsConfig, - useGlobalStatusInterval: v, - }) - } - /> - - )} - {form.statsConfig.statusCheckEnabled && - !form.statsConfig.useGlobalStatusInterval && ( - - - setField("statsConfig", { - ...form.statsConfig, - statusCheckInterval: Number(e.target.value), - }) - } - className="w-20 h-7 text-xs text-right [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" - /> - - )} -
-
} diff --git a/plugins/remote-desktop/src/frontend/remote-form.ts b/plugins/remote-desktop/src/frontend/remote-form.ts index 96e833041..257fdd1ca 100644 --- a/plugins/remote-desktop/src/frontend/remote-form.ts +++ b/plugins/remote-desktop/src/frontend/remote-form.ts @@ -28,7 +28,6 @@ export interface RemoteDesktopForm { telnetCredentialId: string; telnetUser: string; telnetPassword: string; - statsConfig: Record; } export type RemoteFormSetField = ( diff --git a/plugins/snippets/src/frontend/index.tsx b/plugins/snippets/src/frontend/index.tsx index dfb7f1ab7..2c2b1ffdb 100644 --- a/plugins/snippets/src/frontend/index.tsx +++ b/plugins/snippets/src/frontend/index.tsx @@ -64,4 +64,26 @@ export function activate(app: TermixApp): void { isNote: snippet.isNote, }; }) as never); + + // For other plugins' quick actions (host metrics): the snippet's content, + // so the caller can ask for $INPUT_n values, and a run on a host. + app.registerAction("snippets.get", (async (snippetId: number) => { + const snippet = await createSnippetsApi(app.api) + .get(snippetId) + .catch(() => null); + return snippet + ? { id: snippet.id, name: snippet.name, content: snippet.content } + : null; + }) as never); + + app.registerAction("snippets.execute", (async ( + snippetId: number, + hostId: number, + inputValues?: Record, + ) => + createSnippetsApi(app.api).execute( + snippetId, + hostId, + inputValues, + )) as never); } diff --git a/plugins/tailscale/manifest.json b/plugins/tailscale/manifest.json index f04cb871f..7e3a5ca30 100644 --- a/plugins/tailscale/manifest.json +++ b/plugins/tailscale/manifest.json @@ -15,6 +15,7 @@ "api": "1" }, "capabilities": [ + "hosts:read", "settings:read-core", "network:outbound", "network:serve", diff --git a/plugins/tailscale/src/backend/host-metrics-manager.ts b/plugins/tailscale/src/backend/host-metrics-manager.ts index 8f82c39fc..3b03ddb15 100644 --- a/plugins/tailscale/src/backend/host-metrics-manager.ts +++ b/plugins/tailscale/src/backend/host-metrics-manager.ts @@ -1,15 +1,15 @@ import type { Router } from "express"; import type { Request, Response } from "express"; import type { PluginContext } from "@termix/plugin-sdk/backend"; -import type { AuthenticatedRequest } from "../../../../src/types/index.js"; -import { DataCrypto } from "../../../../src/backend/utils/data-crypto.js"; -import { PermissionManager } from "../../../../src/backend/utils/permission-manager.js"; -import { resolveHostById } from "../../../../src/backend/hosts/host-resolver.js"; -import { execCommand } from "../../../../src/backend/hosts/metrics-shared/common-utils.js"; -import { execElevated } from "../../../../src/backend/hosts/metrics-shared/exec-elevated.js"; -import { isValidTailscaleAction } from "../../../../src/backend/hosts/metrics-shared/validation.js"; +import { execCommand, execElevated } from "@termix/plugin-sdk/host-commands"; import { withSshConnection } from "./ssh.js"; +export type TailscaleAction = "up" | "down"; + +export function isValidTailscaleAction(a: unknown): a is TailscaleAction { + return a === "up" || a === "down"; +} + export interface TailscalePeer { hostname: string; tailscaleIPs: string[]; @@ -116,32 +116,29 @@ interface ManagerHost { sudoPassword?: string; } +/** + * The host as the acting user may reach it. The resolved host carries the + * owner's sudo password even for a shared user: core decrypts it with the + * owner's key. + */ async function resolveManagerHost( + ctx: PluginContext, hostId: number, - userId: string, ): Promise { - const access = await PermissionManager.getInstance().canAccessHost( - userId, - hostId, - "connect", - ); + const access = await ctx.hosts.checkAccess(hostId, "connect"); if (!access.hasAccess) throw new AccessDeniedError(); - - if (DataCrypto.getUserDataKey(userId) === null) { - throw new AccessDeniedError(); - } - const host = await resolveHostById(hostId, userId); + const host = await ctx.ssh.resolveHost(hostId); if (!host) throw new AccessDeniedError("Host not found"); - - let sudoPassword = (host as { sudoPassword?: string }).sudoPassword; - const ownerId = (host as { userId?: string }).userId; - if (ownerId && ownerId !== userId) { - const ownerHost = await resolveHostById(hostId, ownerId); - sudoPassword = (ownerHost as { sudoPassword?: string } | null) - ?.sudoPassword; - } - - return { id: hostId, userId, sudoPassword }; + const terminalConfig = host.terminalConfig as + { sudoPassword?: string } | undefined; + return { + id: hostId, + userId: String(host.userId ?? ""), + sudoPassword: + (host.sudoPassword as string | undefined) || + terminalConfig?.sudoPassword || + undefined, + }; } function managerErrorResponse( @@ -183,10 +180,9 @@ export function registerTailscaleHostMetricsManager( router.get( "/host-metrics-manager/:id", async (req: Request, res: Response) => { - const userId = (req as AuthenticatedRequest).userId; const hostId = parseInt(String(req.params.id), 10); try { - const host = await resolveManagerHost(hostId, userId); + const host = await resolveManagerHost(ctx, hostId); const data = await withSshConnection( host.id, { pool: "tailscale", purpose: "metrics" }, @@ -236,10 +232,9 @@ export function registerTailscaleHostMetricsManager( router.post( "/host-metrics-manager/:id/action", async (req: Request, res: Response) => { - const userId = (req as AuthenticatedRequest).userId; const hostId = parseInt(String(req.params.id), 10); try { - const host = await resolveManagerHost(hostId, userId); + const host = await resolveManagerHost(ctx, hostId); const { action } = req.body as { action: unknown }; if (!isValidTailscaleAction(action)) { throw new ManagerInputError("Invalid action, must be 'up' or 'down'"); diff --git a/scripts/plugin-boundary-allowlist.json b/scripts/plugin-boundary-allowlist.json index 8d5195d0c..610d9c4a3 100644 --- a/scripts/plugin-boundary-allowlist.json +++ b/scripts/plugin-boundary-allowlist.json @@ -72,42 +72,6 @@ "plugins/file-manager/src/frontend/optimistic-file-list.ts", "plugins/file-manager/src/frontend/transferMetricsFormat.ts", "plugins/file-manager/src/frontend/transferProgressMonitor.tsx", - "plugins/host-metrics/src/frontend/HostEditorStatsTab.tsx", - "plugins/host-metrics/src/frontend/HostMetricsApp.tsx", - "plugins/host-metrics/src/frontend/HostMetricsTab.tsx", - "plugins/host-metrics/src/frontend/MetricsChartEditForm.tsx", - "plugins/host-metrics/src/frontend/MetricsChartWidget.tsx", - "plugins/host-metrics/src/frontend/cards/CardTimeTabs.tsx", - "plugins/host-metrics/src/frontend/cards/CpuCard.tsx", - "plugins/host-metrics/src/frontend/cards/DiskCard.tsx", - "plugins/host-metrics/src/frontend/cards/FilesystemPicker.tsx", - "plugins/host-metrics/src/frontend/cards/FirewallCard.tsx", - "plugins/host-metrics/src/frontend/cards/GpuCard.tsx", - "plugins/host-metrics/src/frontend/cards/LoginStatsCard.tsx", - "plugins/host-metrics/src/frontend/cards/MemoryCard.tsx", - "plugins/host-metrics/src/frontend/cards/NetworkCard.tsx", - "plugins/host-metrics/src/frontend/cards/PortsCard.tsx", - "plugins/host-metrics/src/frontend/cards/ProcessesCard.tsx", - "plugins/host-metrics/src/frontend/cards/SystemCard.tsx", - "plugins/host-metrics/src/frontend/cards/TemperatureCard.tsx", - "plugins/host-metrics/src/frontend/cards/UptimeCard.tsx", - "plugins/host-metrics/src/frontend/cards/gpu-history.ts", - "plugins/host-metrics/src/frontend/cards/index.tsx", - "plugins/host-metrics/src/frontend/cards/managers/CronManagerCard.tsx", - "plugins/host-metrics/src/frontend/cards/managers/FirewallManagerCard.tsx", - "plugins/host-metrics/src/frontend/cards/managers/HealthCheckCard.tsx", - "plugins/host-metrics/src/frontend/cards/managers/LogViewerCard.tsx", - "plugins/host-metrics/src/frontend/cards/managers/ManagerCardShell.tsx", - "plugins/host-metrics/src/frontend/cards/managers/PackageManagerCard.tsx", - "plugins/host-metrics/src/frontend/cards/managers/ProcessInspectorCard.tsx", - "plugins/host-metrics/src/frontend/cards/managers/SimpleManagerCards.tsx", - "plugins/host-metrics/src/frontend/cards/managers/SslManagerCard.tsx", - "plugins/host-metrics/src/frontend/cards/managers/UserManagerCard.tsx", - "plugins/host-metrics/src/frontend/hooks/useHostMetricsPreferences.ts", - "plugins/host-metrics/src/frontend/host-metrics-api.ts", - "plugins/host-metrics/src/frontend/index.tsx", - "plugins/host-metrics/src/frontend/metrics-change-key.ts", - "plugins/host-metrics/src/frontend/temperature-preference.ts", "plugins/proxmox/src/frontend/HostProxmoxTab.tsx", "plugins/proxmox/src/frontend/ProxmoxDiscoverDialog.tsx", "plugins/proxmox/src/frontend/index.tsx", @@ -150,46 +114,8 @@ "plugins/docker/src/backend/session-manager.ts", "plugins/file-manager/src/backend/index.ts", "plugins/file-manager/src/backend/operation-routes.ts", - "plugins/host-metrics/src/backend/automation-bridge.ts", - "plugins/host-metrics/src/backend/history-routes.ts", - "plugins/host-metrics/src/backend/managers/cron.ts", - "plugins/host-metrics/src/backend/managers/firewall.ts", - "plugins/host-metrics/src/backend/managers/health.ts", - "plugins/host-metrics/src/backend/managers/index.ts", - "plugins/host-metrics/src/backend/managers/logs.ts", - "plugins/host-metrics/src/backend/managers/packages.ts", - "plugins/host-metrics/src/backend/managers/processes.ts", - "plugins/host-metrics/src/backend/managers/route-helpers.ts", - "plugins/host-metrics/src/backend/managers/services.ts", - "plugins/host-metrics/src/backend/managers/simple-reads.ts", - "plugins/host-metrics/src/backend/managers/ssl.ts", - "plugins/host-metrics/src/backend/managers/types.ts", - "plugins/host-metrics/src/backend/managers/users.ts", - "plugins/host-metrics/src/backend/managers/wireguard.ts", - "plugins/host-metrics/src/backend/preferences-routes.ts", - "plugins/host-metrics/src/backend/routes.ts", - "plugins/host-metrics/src/backend/sessions.ts", - "plugins/host-metrics/src/backend/settings-routes.ts", - "plugins/host-metrics/src/backend/viewer-routes.ts", - "plugins/host-metrics/src/backend/widgets/cpu-collector.ts", - "plugins/host-metrics/src/backend/widgets/disk-collector.ts", - "plugins/host-metrics/src/backend/widgets/firewall-collector.ts", - "plugins/host-metrics/src/backend/widgets/gpu-collector.ts", - "plugins/host-metrics/src/backend/widgets/login-stats-collector.ts", - "plugins/host-metrics/src/backend/widgets/memory-collector.ts", - "plugins/host-metrics/src/backend/widgets/network-collector.ts", - "plugins/host-metrics/src/backend/widgets/ports-collector.ts", - "plugins/host-metrics/src/backend/widgets/processes-collector.ts", - "plugins/host-metrics/src/backend/widgets/system-collector.ts", - "plugins/host-metrics/src/backend/widgets/temperature-collector.ts", - "plugins/host-metrics/src/backend/widgets/uptime-collector.ts", "plugins/proxmox/src/backend/routes.ts", - "plugins/tailscale/src/backend/host-metrics-manager.ts", "plugins/tailscale/src/backend/routes.ts" ], - "plugin-to-plugin": [ - "plugins/ai/src/backend/tools/executor.ts", - "plugins/host-metrics/src/backend/automation-bridge.ts", - "plugins/host-metrics/src/backend/routes.ts" - ] + "plugin-to-plugin": ["plugins/ai/src/backend/tools/executor.ts"] } diff --git a/src/backend/database/database.ts b/src/backend/database/database.ts index c4b99ffbf..f5c76a9cc 100644 --- a/src/backend/database/database.ts +++ b/src/backend/database/database.ts @@ -806,7 +806,8 @@ app.post("/database/export", authenticateJWT, async (req, res) => { show_docker_in_sidebar INTEGER NOT NULL DEFAULT 0, show_server_stats_in_sidebar INTEGER NOT NULL DEFAULT 0, default_path TEXT, - stats_config TEXT, + status_check_enabled INTEGER NOT NULL DEFAULT 1, + status_check_interval INTEGER, docker_config TEXT, web_ui_config TEXT, terminal_config TEXT, @@ -928,8 +929,8 @@ app.post("/database/export", authenticateJWT, async (req, res) => { const sshHosts = await createCurrentHostRepository().listDecryptedByUserId(userId); const insertHost = exportDb.prepare(` - INSERT INTO ssh_data (id, user_id, connection_type, name, ip, port, username, folder, tags, pin, auth_type, force_keyboard_interactive, password, key, key_password, key_type, sudo_password, autostart_password, autostart_key, autostart_key_password, credential_id, override_credential_username, enable_terminal, enable_tunnel, tunnel_connections, jump_hosts, enable_file_manager, enable_docker, enable_web_ui, show_terminal_in_sidebar, show_file_manager_in_sidebar, show_tunnel_in_sidebar, show_docker_in_sidebar, show_server_stats_in_sidebar, default_path, stats_config, docker_config, web_ui_config, terminal_config, quick_actions, notes, use_socks5, socks5_host, socks5_port, socks5_username, socks5_password, socks5_proxy_chain, domain, mac_address, port_knock_sequence, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO ssh_data (id, user_id, connection_type, name, ip, port, username, folder, tags, pin, auth_type, force_keyboard_interactive, password, key, key_password, key_type, sudo_password, autostart_password, autostart_key, autostart_key_password, credential_id, override_credential_username, enable_terminal, enable_tunnel, tunnel_connections, jump_hosts, enable_file_manager, enable_docker, enable_web_ui, show_terminal_in_sidebar, show_file_manager_in_sidebar, show_tunnel_in_sidebar, show_docker_in_sidebar, show_server_stats_in_sidebar, default_path, status_check_enabled, status_check_interval, docker_config, web_ui_config, terminal_config, quick_actions, notes, use_socks5, socks5_host, socks5_port, socks5_username, socks5_password, socks5_proxy_chain, domain, mac_address, port_knock_sequence, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); for (const decrypted of sshHosts) { @@ -969,7 +970,8 @@ app.post("/database/export", authenticateJWT, async (req, res) => { decrypted.showDockerInSidebar ? 1 : 0, decrypted.showServerStatsInSidebar ? 1 : 0, decrypted.defaultPath || null, - decrypted.statsConfig || null, + decrypted.statusCheckEnabled === false ? 0 : 1, + decrypted.statusCheckInterval ?? null, decrypted.dockerConfig || null, decrypted.webUiConfig || null, decrypted.terminalConfig || null, @@ -1378,7 +1380,7 @@ app.post( host.show_server_stats_in_sidebar, ), defaultPath: host.default_path, - statsConfig: host.stats_config, + ...legacyStatusCheck(host), terminalConfig: host.terminal_config, quickActions: host.quick_actions, notes: host.notes, @@ -2156,3 +2158,42 @@ if ( ); } } + +/** + * Status check columns from an export row. Exports from before 2.9.0 only + * have stats_config, which carried them. + */ +function legacyStatusCheck(row: Record): { + statusCheckEnabled: boolean; + statusCheckInterval: number | null; +} { + if (row.status_check_enabled !== undefined) { + return { + statusCheckEnabled: Boolean(row.status_check_enabled), + statusCheckInterval: + typeof row.status_check_interval === "number" + ? row.status_check_interval + : null, + }; + } + let legacy: Record = {}; + try { + legacy = + typeof row.stats_config === "string" && row.stats_config + ? JSON.parse(row.stats_config) + : {}; + } catch { + legacy = {}; + } + const seconds = Number(legacy.statusCheckInterval); + return { + statusCheckEnabled: + legacy.statusCheckEnabled !== false && legacy.disableTcpPing !== true, + statusCheckInterval: + legacy.useGlobalStatusInterval === false && + Number.isInteger(seconds) && + seconds >= 5 + ? seconds + : null, + }; +} diff --git a/src/backend/database/db/index.ts b/src/backend/database/db/index.ts index 63dff7cc0..654d5b68e 100644 --- a/src/backend/database/db/index.ts +++ b/src/backend/database/db/index.ts @@ -334,7 +334,8 @@ async function initializeCompleteDatabase(): Promise { autostart_key TEXT, autostart_key_password TEXT, force_keyboard_interactive TEXT, - stats_config TEXT, + status_check_enabled INTEGER NOT NULL DEFAULT 1, + status_check_interval INTEGER, docker_config TEXT, web_ui_config TEXT, terminal_config TEXT, @@ -687,20 +688,6 @@ async function initializeCompleteDatabase(): Promise { FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE ); - CREATE TABLE IF NOT EXISTS host_metrics_preferences ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - host_id INTEGER NOT NULL, - layout TEXT NOT NULL, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, - FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE - ); - - CREATE UNIQUE INDEX IF NOT EXISTS idx_host_metrics_prefs_user_host - ON host_metrics_preferences (user_id, host_id); - CREATE TABLE IF NOT EXISTS host_sidebar_preferences ( user_id TEXT PRIMARY KEY, data TEXT NOT NULL, @@ -722,37 +709,6 @@ async function initializeCompleteDatabase(): Promise { FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE ); - CREATE TABLE IF NOT EXISTS host_health_checks ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - host_id INTEGER NOT NULL, - checks TEXT NOT NULL, - interval_seconds INTEGER NOT NULL DEFAULT 300, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, - FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE - ); - - CREATE UNIQUE INDEX IF NOT EXISTS idx_host_health_checks_user_host - ON host_health_checks (user_id, host_id); - - CREATE TABLE IF NOT EXISTS host_health_history ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - host_id INTEGER NOT NULL, - check_id TEXT NOT NULL, - ts TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - ok INTEGER NOT NULL, - latency_ms INTEGER, - detail TEXT, - 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_host_health_history_lookup - ON host_health_history (user_id, host_id, check_id, ts); - CREATE TABLE IF NOT EXISTS sso_providers ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, @@ -1111,7 +1067,12 @@ const migrateSchema = () => { addColumnIfNotExists("ssh_data", "autostart_password", "TEXT"); addColumnIfNotExists("ssh_data", "autostart_key", "TEXT"); addColumnIfNotExists("ssh_data", "autostart_key_password", "TEXT"); - addColumnIfNotExists("ssh_data", "stats_config", "TEXT"); + addColumnIfNotExists( + "ssh_data", + "status_check_enabled", + "INTEGER NOT NULL DEFAULT 1", + ); + addColumnIfNotExists("ssh_data", "status_check_interval", "INTEGER"); addColumnIfNotExists("ssh_data", "terminal_config", "TEXT"); addColumnIfNotExists("ssh_data", "quick_actions", "TEXT"); addColumnIfNotExists( @@ -2034,34 +1995,6 @@ const migrateSchema = () => { }); } - // --- metrics-history begin --- - try { - sqlite.prepare("SELECT id FROM host_metrics_history LIMIT 1").get(); - } catch { - try { - sqlite.exec(` - CREATE TABLE IF NOT EXISTS host_metrics_history ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - host_id INTEGER NOT NULL REFERENCES ssh_data(id) ON DELETE CASCADE, - ts TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - cpu_percent REAL, - mem_percent REAL, - disk_percent REAL, - net_rx_bytes INTEGER, - net_tx_bytes INTEGER - ); - CREATE INDEX IF NOT EXISTS idx_host_metrics_history_host_ts - ON host_metrics_history (host_id, ts DESC); - `); - } catch (createError) { - databaseLogger.warn("Failed to create host_metrics_history table", { - operation: "schema_migration", - error: createError, - }); - } - } - // --- metrics-history end --- - // --- alerts begin --- // alert_rules, alert_rule_channels and alert_firings were only ever created // here, never declared in schema.ts, so they never existed on Postgres or @@ -2258,19 +2191,6 @@ const migrateSchema = () => { } // --- automations end --- - // Seed default metrics history retention setting - try { - ensureRawSettingDefault("metrics_history_retention_days", "7"); - } catch (e) { - databaseLogger.warn( - "Could not initialize metrics_history_retention_days setting", - { - operation: "schema_migration", - error: e, - }, - ); - } - // --- homepage begin --- try { sqlite.prepare("SELECT id FROM homepage_items LIMIT 1").get(); diff --git a/src/backend/database/db/schema.mysql.ts b/src/backend/database/db/schema.mysql.ts index 6c8e15390..00c114173 100644 --- a/src/backend/database/db/schema.mysql.ts +++ b/src/backend/database/db/schema.mysql.ts @@ -295,7 +295,10 @@ export const hosts = mysqlTable( .notNull() .default(false), defaultPath: text("default_path"), - statsConfig: text("stats_config"), + statusCheckEnabled: boolean("status_check_enabled") + .notNull() + .default(true), + statusCheckInterval: int("status_check_interval"), dockerConfig: text("docker_config"), webUiConfig: text("web_ui_config"), terminalConfig: text("terminal_config"), @@ -887,40 +890,12 @@ export const userPreferences = mysqlTable("user_preferences", { .default(sql`(CURRENT_TIMESTAMP)`), }); -export const hostMetricsPreferences = mysqlTable( - "host_metrics_preferences", - { - 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" }), - // JSON-encoded HostMetricsLayout. Layout has no secrets, so it is stored as - // plain JSON (no field-level encryption). - layout: text("layout").notNull(), - createdAt: varchar("created_at", { length: 255 }) - .notNull() - .default(sql`(CURRENT_TIMESTAMP)`), - updatedAt: varchar("updated_at", { length: 255 }) - .notNull() - .default(sql`(CURRENT_TIMESTAMP)`), - }, - // One layout per user per host. Enforced in production since the inline DDL - // creates it, but it was never declared here, so the generated Postgres and - // MySQL schemas lacked it — and the upsert has nothing to conflict on. - (table) => [ - uniqueIndex("idx_host_metrics_prefs_user_host").on(table.userId, table.hostId), - ], -); - export const hostSidebarPreferences = mysqlTable("host_sidebar_preferences", { userId: varchar("user_id", { length: 255 }) .primaryKey() .references(() => users.id, { onDelete: "cascade" }), // JSON-encoded HostSidebarPreferences. No secrets in this blob, stored as - // plain JSON like hostMetricsPreferences.layout. + // plain JSON. data: text("data").notNull(), updatedAt: varchar("updated_at", { length: 255 }) .notNull() @@ -955,47 +930,6 @@ export const uiPreferences = mysqlTable("ui_preferences", { .default(sql`(CURRENT_TIMESTAMP)`), }); -export const hostHealthChecks = mysqlTable( - "host_health_checks", - { - 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" }), - // JSON array of { id, name, type: "tcp"|"http", target, port, path } - checks: text("checks").notNull(), - intervalSeconds: int("interval_seconds").notNull().default(300), - createdAt: varchar("created_at", { length: 255 }) - .notNull() - .default(sql`(CURRENT_TIMESTAMP)`), - updatedAt: varchar("updated_at", { length: 255 }) - .notNull() - .default(sql`(CURRENT_TIMESTAMP)`), - }, - // Same as above: one set of checks per user per host. - (table) => [ - uniqueIndex("idx_host_health_checks_user_host").on(table.userId, table.hostId), - ], -); - -export const hostHealthHistory = mysqlTable("host_health_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" }), - checkId: text("check_id").notNull(), - ts: text("ts").notNull().default(sql`(CURRENT_TIMESTAMP)`), - ok: boolean("ok").notNull(), - latencyMs: int("latency_ms"), - detail: text("detail"), -}); - export const dashboardServiceLinks = mysqlTable("dashboard_service_links", { id: int("id").autoincrement().primaryKey(), userId: varchar("user_id", { length: 255 }) @@ -1086,23 +1020,6 @@ export const termixIdentityCa = mysqlTable("termix_identity_ca", { }); // --- termix-id end --- -// --- metrics-history begin --- -export const hostMetricsHistory = mysqlTable("host_metrics_history", { - id: int("id").autoincrement().primaryKey(), - hostId: int("host_id") - .notNull() - .references(() => hosts.id, { onDelete: "cascade" }), - ts: text("ts") - .notNull() - .default(sql`(CURRENT_TIMESTAMP)`), - cpuPercent: double("cpu_percent"), - memPercent: double("mem_percent"), - diskPercent: double("disk_percent"), - netRxBytes: int("net_rx_bytes"), - netTxBytes: int("net_tx_bytes"), -}); -// --- metrics-history end --- - // --- alerts begin --- export const notificationChannels = mysqlTable("notification_channels", { id: int("id").autoincrement().primaryKey(), diff --git a/src/backend/database/db/schema.pg.ts b/src/backend/database/db/schema.pg.ts index b31a66c3d..6547794da 100644 --- a/src/backend/database/db/schema.pg.ts +++ b/src/backend/database/db/schema.pg.ts @@ -296,7 +296,10 @@ export const hosts = pgTable( .notNull() .default(false), defaultPath: text("default_path"), - statsConfig: text("stats_config"), + statusCheckEnabled: boolean("status_check_enabled") + .notNull() + .default(true), + statusCheckInterval: integer("status_check_interval"), dockerConfig: text("docker_config"), webUiConfig: text("web_ui_config"), terminalConfig: text("terminal_config"), @@ -888,40 +891,12 @@ export const userPreferences = pgTable("user_preferences", { .default(sql`CURRENT_TIMESTAMP`), }); -export const hostMetricsPreferences = pgTable( - "host_metrics_preferences", - { - 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" }), - // JSON-encoded HostMetricsLayout. Layout has no secrets, so it is stored as - // plain JSON (no field-level encryption). - layout: text("layout").notNull(), - createdAt: varchar("created_at", { length: 255 }) - .notNull() - .default(sql`CURRENT_TIMESTAMP`), - updatedAt: varchar("updated_at", { length: 255 }) - .notNull() - .default(sql`CURRENT_TIMESTAMP`), - }, - // One layout per user per host. Enforced in production since the inline DDL - // creates it, but it was never declared here, so the generated Postgres and - // MySQL schemas lacked it — and the upsert has nothing to conflict on. - (table) => [ - uniqueIndex("idx_host_metrics_prefs_user_host").on(table.userId, table.hostId), - ], -); - export const hostSidebarPreferences = pgTable("host_sidebar_preferences", { userId: varchar("user_id", { length: 255 }) .primaryKey() .references(() => users.id, { onDelete: "cascade" }), // JSON-encoded HostSidebarPreferences. No secrets in this blob, stored as - // plain JSON like hostMetricsPreferences.layout. + // plain JSON. data: text("data").notNull(), updatedAt: varchar("updated_at", { length: 255 }) .notNull() @@ -956,47 +931,6 @@ export const uiPreferences = pgTable("ui_preferences", { .default(sql`CURRENT_TIMESTAMP`), }); -export const hostHealthChecks = pgTable( - "host_health_checks", - { - 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" }), - // JSON array of { id, name, type: "tcp"|"http", target, port, path } - checks: text("checks").notNull(), - intervalSeconds: integer("interval_seconds").notNull().default(300), - createdAt: varchar("created_at", { length: 255 }) - .notNull() - .default(sql`CURRENT_TIMESTAMP`), - updatedAt: varchar("updated_at", { length: 255 }) - .notNull() - .default(sql`CURRENT_TIMESTAMP`), - }, - // Same as above: one set of checks per user per host. - (table) => [ - uniqueIndex("idx_host_health_checks_user_host").on(table.userId, table.hostId), - ], -); - -export const hostHealthHistory = pgTable("host_health_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" }), - checkId: text("check_id").notNull(), - ts: text("ts").notNull().default(sql`CURRENT_TIMESTAMP`), - ok: boolean("ok").notNull(), - latencyMs: integer("latency_ms"), - detail: text("detail"), -}); - export const dashboardServiceLinks = pgTable("dashboard_service_links", { id: serial("id").primaryKey(), userId: varchar("user_id", { length: 255 }) @@ -1087,23 +1021,6 @@ export const termixIdentityCa = pgTable("termix_identity_ca", { }); // --- termix-id end --- -// --- metrics-history begin --- -export const hostMetricsHistory = pgTable("host_metrics_history", { - id: serial("id").primaryKey(), - hostId: integer("host_id") - .notNull() - .references(() => hosts.id, { onDelete: "cascade" }), - ts: text("ts") - .notNull() - .default(sql`CURRENT_TIMESTAMP`), - cpuPercent: doublePrecision("cpu_percent"), - memPercent: doublePrecision("mem_percent"), - diskPercent: doublePrecision("disk_percent"), - netRxBytes: integer("net_rx_bytes"), - netTxBytes: integer("net_tx_bytes"), -}); -// --- metrics-history end --- - // --- alerts begin --- export const notificationChannels = pgTable("notification_channels", { id: serial("id").primaryKey(), diff --git a/src/backend/database/db/schema.ts b/src/backend/database/db/schema.ts index 8daf477ba..1ee79044a 100644 --- a/src/backend/database/db/schema.ts +++ b/src/backend/database/db/schema.ts @@ -288,7 +288,10 @@ export const hosts = sqliteTable( .notNull() .default(false), defaultPath: text("default_path"), - statsConfig: text("stats_config"), + statusCheckEnabled: integer("status_check_enabled", { mode: "boolean" }) + .notNull() + .default(true), + statusCheckInterval: integer("status_check_interval"), dockerConfig: text("docker_config"), webUiConfig: text("web_ui_config"), terminalConfig: text("terminal_config"), @@ -884,40 +887,12 @@ export const userPreferences = sqliteTable("user_preferences", { .default(sql`CURRENT_TIMESTAMP`), }); -export const hostMetricsPreferences = sqliteTable( - "host_metrics_preferences", - { - 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" }), - // JSON-encoded HostMetricsLayout. Layout has no secrets, so it is stored as - // plain JSON (no field-level encryption). - layout: text("layout").notNull(), - createdAt: text("created_at") - .notNull() - .default(sql`CURRENT_TIMESTAMP`), - updatedAt: text("updated_at") - .notNull() - .default(sql`CURRENT_TIMESTAMP`), - }, - // One layout per user per host. Enforced in production since the inline DDL - // creates it, but it was never declared here, so the generated Postgres and - // MySQL schemas lacked it — and the upsert has nothing to conflict on. - (table) => [ - uniqueIndex("idx_host_metrics_prefs_user_host").on(table.userId, table.hostId), - ], -); - export const hostSidebarPreferences = sqliteTable("host_sidebar_preferences", { userId: text("user_id") .primaryKey() .references(() => users.id, { onDelete: "cascade" }), // JSON-encoded HostSidebarPreferences. No secrets in this blob, stored as - // plain JSON like hostMetricsPreferences.layout. + // plain JSON. data: text("data").notNull(), updatedAt: text("updated_at") .notNull() @@ -952,47 +927,6 @@ export const uiPreferences = sqliteTable("ui_preferences", { .default(sql`CURRENT_TIMESTAMP`), }); -export const hostHealthChecks = sqliteTable( - "host_health_checks", - { - 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" }), - // JSON array of { id, name, type: "tcp"|"http", target, port, path } - checks: text("checks").notNull(), - intervalSeconds: integer("interval_seconds").notNull().default(300), - createdAt: text("created_at") - .notNull() - .default(sql`CURRENT_TIMESTAMP`), - updatedAt: text("updated_at") - .notNull() - .default(sql`CURRENT_TIMESTAMP`), - }, - // Same as above: one set of checks per user per host. - (table) => [ - uniqueIndex("idx_host_health_checks_user_host").on(table.userId, table.hostId), - ], -); - -export const hostHealthHistory = sqliteTable("host_health_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" }), - checkId: text("check_id").notNull(), - ts: text("ts").notNull().default(sql`CURRENT_TIMESTAMP`), - ok: integer("ok", { mode: "boolean" }).notNull(), - latencyMs: integer("latency_ms"), - detail: text("detail"), -}); - export const dashboardServiceLinks = sqliteTable("dashboard_service_links", { id: integer("id").primaryKey({ autoIncrement: true }), userId: text("user_id") @@ -1083,23 +1017,6 @@ export const termixIdentityCa = sqliteTable("termix_identity_ca", { }); // --- termix-id end --- -// --- metrics-history begin --- -export const hostMetricsHistory = sqliteTable("host_metrics_history", { - id: integer("id").primaryKey({ autoIncrement: true }), - hostId: integer("host_id") - .notNull() - .references(() => hosts.id, { onDelete: "cascade" }), - ts: text("ts") - .notNull() - .default(sql`CURRENT_TIMESTAMP`), - cpuPercent: real("cpu_percent"), - memPercent: real("mem_percent"), - diskPercent: real("disk_percent"), - netRxBytes: integer("net_rx_bytes"), - netTxBytes: integer("net_tx_bytes"), -}); -// --- metrics-history end --- - // --- alerts begin --- export const notificationChannels = sqliteTable("notification_channels", { id: integer("id").primaryKey({ autoIncrement: true }), diff --git a/src/backend/database/repositories/factory.ts b/src/backend/database/repositories/factory.ts index 20b9f485e..6a341b7bf 100644 --- a/src/backend/database/repositories/factory.ts +++ b/src/backend/database/repositories/factory.ts @@ -15,9 +15,6 @@ import { DismissedAlertRepository } from "./dismissed-alert-repository.js"; import { HomepageItemRepository } from "./homepage-item-repository.js"; import { HomepageLayoutRepository } from "./homepage-layout-repository.js"; import { HostFolderRepository } from "./host-folder-repository.js"; -import { HostHealthRepository } from "./host-health-repository.js"; -import { HostMetricsHistoryRepository } from "./host-metrics-history-repository.js"; -import { HostMetricsPreferenceRepository } from "./host-metrics-preference-repository.js"; import { HostRepository } from "./host-repository.js"; import { HostResolutionRepository } from "./host-resolution-repository.js"; import { HostSidebarPreferenceRepository } from "./host-sidebar-preference-repository.js"; @@ -230,31 +227,6 @@ export function createCurrentHostFolderRepository(): HostFolderRepository { ); } -export function createCurrentHostHealthRepository(): HostHealthRepository { - return new HostHealthRepository( - createCurrentRepositoryContext(), - createCurrentRepositoryWriteHook("host_health_repository_write"), - ); -} - -export function createCurrentHostMetricsHistoryRepository(): HostMetricsHistoryRepository { - return new HostMetricsHistoryRepository( - createCurrentRepositoryContext(), - createCurrentRepositoryLazyWriteHook( - "host_metrics_history_repository_write", - ), - ); -} - -export function createCurrentHostMetricsPreferenceRepository(): HostMetricsPreferenceRepository { - return new HostMetricsPreferenceRepository( - createCurrentRepositoryContext(), - createCurrentRepositoryWriteHook( - "host_metrics_preference_repository_write", - ), - ); -} - export function createCurrentHostRepository(): HostRepository { return new HostRepository( createCurrentRepositoryContext(), diff --git a/src/backend/database/repositories/host-health-repository.ts b/src/backend/database/repositories/host-health-repository.ts deleted file mode 100644 index 875e1d74a..000000000 --- a/src/backend/database/repositories/host-health-repository.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { and, desc, eq, notInArray } from "drizzle-orm"; -import { hostHealthChecks, hostHealthHistory } from "../db/schema.js"; -import type { DatabaseContext } from "./database-context.js"; -import { rowsAffected } from "./mutation-result.js"; -import { insertReturning, updateReturning } from "./returning.js"; - -export type HostHealthCheckRecord = typeof hostHealthChecks.$inferSelect; -export type HostHealthHistoryRecord = typeof hostHealthHistory.$inferSelect; - -export interface HostHealthResultInput { - checkId: string; - ok: boolean; - latencyMs: number | null; - detail: string; -} - -export class HostHealthRepository { - constructor( - private readonly context: DatabaseContext, - private readonly onWrite?: () => void | Promise, - ) {} - - async findChecksByUserAndHost( - userId: string, - hostId: number, - ): Promise { - const rows = await this.context.drizzle - .select() - .from(hostHealthChecks) - .where( - and( - eq(hostHealthChecks.userId, userId), - eq(hostHealthChecks.hostId, hostId), - ), - ) - .limit(1); - - return rows[0] ?? null; - } - - async upsertChecks( - userId: string, - hostId: number, - checks: string, - intervalSeconds: number, - now = new Date().toISOString(), - ): Promise { - const existing = await this.findChecksByUserAndHost(userId, hostId); - if (existing) { - const [updated] = await updateReturning( - this.context, - hostHealthChecks, - { checks, intervalSeconds, updatedAt: now }, - eq(hostHealthChecks.id, existing.id), - ); - - await this.afterWrite(); - return updated; - } - - const [created] = await insertReturning(this.context, hostHealthChecks, { - userId, - hostId, - checks, - intervalSeconds, - createdAt: now, - updatedAt: now, - }); - - await this.afterWrite(); - return created; - } - - async recordHistory( - userId: string, - hostId: number, - results: HostHealthResultInput[], - keep: number, - now = new Date().toISOString(), - ): Promise { - if (results.length === 0) { - return 0; - } - - await this.context.drizzle.insert(hostHealthHistory).values( - results.map((result) => ({ - userId, - hostId, - checkId: result.checkId, - ts: now, - ok: result.ok, - latencyMs: result.latencyMs, - detail: result.detail, - })), - ); - - await this.pruneHistory(userId, hostId, keep); - await this.afterWrite(); - return results.length; - } - - async listHistory( - userId: string, - hostId: number, - limit: number, - ): Promise { - return this.context.drizzle - .select() - .from(hostHealthHistory) - .where( - and( - eq(hostHealthHistory.userId, userId), - eq(hostHealthHistory.hostId, hostId), - ), - ) - .orderBy(desc(hostHealthHistory.ts)) - .limit(limit); - } - - async deleteByUserId(userId: string): Promise<{ - checksDeleted: number; - historyDeleted: number; - }> { - const historyResult = await this.context.drizzle - .delete(hostHealthHistory) - .where(eq(hostHealthHistory.userId, userId)); - - const result = await this.context.drizzle - .delete(hostHealthChecks) - .where(eq(hostHealthChecks.userId, userId)); - - if (rowsAffected(historyResult) > 0 || rowsAffected(result) > 0) { - await this.afterWrite(); - } - - return { - checksDeleted: rowsAffected(result), - historyDeleted: rowsAffected(historyResult), - }; - } - - /** Keeps the newest `keep` rows for the host and drops the rest. */ - private async pruneHistory( - userId: string, - hostId: number, - keep: number, - ): Promise { - const scope = and( - eq(hostHealthHistory.userId, userId), - eq(hostHealthHistory.hostId, hostId), - ); - - const retained = await this.context.drizzle - .select({ id: hostHealthHistory.id }) - .from(hostHealthHistory) - .where(scope) - .orderBy(desc(hostHealthHistory.ts)) - .limit(keep); - - // Nothing retained means nothing to keep back, so the scope alone is the - // delete condition. - await this.context.drizzle.delete(hostHealthHistory).where( - retained.length - ? and( - scope, - notInArray( - hostHealthHistory.id, - retained.map((row) => row.id), - ), - ) - : scope, - ); - } - - private async afterWrite(): Promise { - await this.onWrite?.(); - } -} diff --git a/src/backend/database/repositories/host-metrics-history-repository.ts b/src/backend/database/repositories/host-metrics-history-repository.ts deleted file mode 100644 index 62bdb0e83..000000000 --- a/src/backend/database/repositories/host-metrics-history-repository.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { and, asc, eq, gte, lt, lte } from "drizzle-orm"; -import { hostMetricsHistory } from "../db/schema.js"; -import type { DatabaseContext } from "./database-context.js"; -import { sqlTimestampDaysAgo } from "./sql-timestamp.js"; - -export type HostMetricsHistoryRecord = typeof hostMetricsHistory.$inferSelect; - -export interface HostMetricsHistoryCreateInput { - hostId: number; - cpuPercent?: number | null; - memPercent?: number | null; - diskPercent?: number | null; - netRxBytes?: number | null; - netTxBytes?: number | null; -} - -export class HostMetricsHistoryRepository { - constructor( - private readonly context: DatabaseContext, - private readonly onWrite?: () => void | Promise, - ) {} - - async create(input: HostMetricsHistoryCreateInput): Promise { - await this.context.drizzle.insert(hostMetricsHistory).values({ - hostId: input.hostId, - cpuPercent: input.cpuPercent, - memPercent: input.memPercent, - diskPercent: input.diskPercent, - netRxBytes: input.netRxBytes, - netTxBytes: input.netTxBytes, - }); - - await this.afterWrite(); - } - - async pruneOlderThan(hostId: number, retentionDays: number): Promise { - await this.context.drizzle - .delete(hostMetricsHistory) - .where( - and( - eq(hostMetricsHistory.hostId, hostId), - lt(hostMetricsHistory.ts, sqlTimestampDaysAgo(retentionDays)), - ), - ); - } - - async listRange( - hostId: number, - fromTs: string, - toTs: string, - ): Promise { - return this.context.drizzle - .select() - .from(hostMetricsHistory) - .where( - and( - eq(hostMetricsHistory.hostId, hostId), - gte(hostMetricsHistory.ts, fromTs), - lte(hostMetricsHistory.ts, toTs), - ), - ) - .orderBy(asc(hostMetricsHistory.ts)); - } - - private async afterWrite(): Promise { - await this.onWrite?.(); - } -} diff --git a/src/backend/database/repositories/host-metrics-preference-repository.ts b/src/backend/database/repositories/host-metrics-preference-repository.ts deleted file mode 100644 index 108acf8a3..000000000 --- a/src/backend/database/repositories/host-metrics-preference-repository.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { and, eq } from "drizzle-orm"; -import { hostMetricsPreferences, hosts } from "../db/schema.js"; -import type { DatabaseContext } from "./database-context.js"; -import { rowsAffected } from "./mutation-result.js"; -import { insertReturning, updateReturning } from "./returning.js"; - -export type HostMetricsPreferenceRecord = - typeof hostMetricsPreferences.$inferSelect; - -export class HostMetricsPreferenceRepository { - constructor( - private readonly context: DatabaseContext, - private readonly onWrite?: () => void | Promise, - ) {} - - async findByUserAndHost( - userId: string, - hostId: number, - ): Promise { - const rows = await this.context.drizzle - .select() - .from(hostMetricsPreferences) - .where( - and( - eq(hostMetricsPreferences.userId, userId), - eq(hostMetricsPreferences.hostId, hostId), - ), - ) - .limit(1); - - return rows[0] ?? null; - } - - async upsertLayout( - userId: string, - hostId: number, - layout: string, - now = new Date().toISOString(), - ): Promise { - const existing = await this.findByUserAndHost(userId, hostId); - if (existing) { - const [updated] = await updateReturning( - this.context, - hostMetricsPreferences, - { layout, updatedAt: now }, - eq(hostMetricsPreferences.id, existing.id), - ); - - await this.afterWrite(); - return updated; - } - - const [created] = await insertReturning( - this.context, - hostMetricsPreferences, - { - userId, - hostId, - layout, - createdAt: now, - updatedAt: now, - }, - ); - - await this.afterWrite(); - return created; - } - - async updateHostStatsConfig( - userId: string, - hostId: number, - statsConfig: string, - ): Promise { - const result = await this.context.drizzle - .update(hosts) - .set({ statsConfig }) - .where(and(eq(hosts.id, hostId), eq(hosts.userId, userId))); - - if (rowsAffected(result) === 0) return false; - await this.afterWrite(); - return true; - } - - async deleteByUserId(userId: string): Promise { - const result = await this.context.drizzle - .delete(hostMetricsPreferences) - .where(eq(hostMetricsPreferences.userId, userId)); - - if (rowsAffected(result) > 0) { - await this.afterWrite(); - } - - return rowsAffected(result); - } - - private async afterWrite(): Promise { - await this.onWrite?.(); - } -} diff --git a/src/backend/database/repositories/host-repository.ts b/src/backend/database/repositories/host-repository.ts index ef53babb0..897120886 100644 --- a/src/backend/database/repositories/host-repository.ts +++ b/src/backend/database/repositories/host-repository.ts @@ -15,7 +15,6 @@ export type NewHostRecord = typeof hosts.$inferInsert; export type HostUpdate = Partial>; export interface HostBulkUpdateState { id: number; - statsConfig: string | null; credentialId: number | null; } @@ -232,7 +231,6 @@ export class HostRepository { return this.context.drizzle .select({ id: hosts.id, - statsConfig: hosts.statsConfig, credentialId: hosts.credentialId, }) .from(hosts) diff --git a/src/backend/database/repositories/host-resolution-repository.ts b/src/backend/database/repositories/host-resolution-repository.ts index 960a02971..a93afafa4 100644 --- a/src/backend/database/repositories/host-resolution-repository.ts +++ b/src/backend/database/repositories/host-resolution-repository.ts @@ -57,6 +57,17 @@ export type HostListRow = HostResolutionHostRecord & { expiresAt?: string | null; }; +export interface HostStatusTargetRow { + id: number; + userId: string; + ip: string; + port: number; + connectionType: string; + jumpHosts: string | null; + statusCheckEnabled: boolean; + statusCheckInterval: number | null; +} + export class HostResolutionRepository { constructor( private readonly context: DatabaseContext, @@ -262,6 +273,34 @@ export class HostResolutionRepository { return rows.length > 0; } + /** + * What the status checker needs, without decrypting anything: none of these + * columns are encrypted. Filtered by owner, by id, or both. + */ + async listStatusTargets(filter: { + userId?: string; + hostIds?: number[]; + }): Promise { + if (filter.hostIds && filter.hostIds.length === 0) return []; + const conditions = [ + filter.userId ? eq(hosts.userId, filter.userId) : undefined, + filter.hostIds ? inArray(hosts.id, filter.hostIds) : undefined, + ].filter((condition) => condition !== undefined); + return this.context.drizzle + .select({ + id: hosts.id, + userId: hosts.userId, + ip: hosts.ip, + port: hosts.port, + connectionType: hosts.connectionType, + jumpHosts: hosts.jumpHosts, + statusCheckEnabled: hosts.statusCheckEnabled, + statusCheckInterval: hosts.statusCheckInterval, + }) + .from(hosts) + .where(conditions.length > 0 ? and(...conditions) : undefined); + } + async listAllHosts(): Promise { const rows = await this.context.drizzle.select().from(hosts); diff --git a/src/backend/database/routes/delete-user-data.ts b/src/backend/database/routes/delete-user-data.ts index 8abef13ed..f8dfe83af 100644 --- a/src/backend/database/routes/delete-user-data.ts +++ b/src/backend/database/routes/delete-user-data.ts @@ -9,9 +9,7 @@ import { createCurrentDismissedAlertRepository, createCurrentHomepageItemRepository, createCurrentHomepageLayoutRepository, - createCurrentHostHealthRepository, createCurrentHostFolderRepository, - createCurrentHostMetricsPreferenceRepository, createCurrentHostRepository, createCurrentHostSidebarPreferenceRepository, createCurrentCredentialSidebarPreferenceRepository, @@ -93,8 +91,6 @@ export async function deleteUserAndRelatedData( await createCurrentHostFolderRepository().deleteByUserId(userId); - await createCurrentHostHealthRepository().deleteByUserId(userId); - await createCurrentHostMetricsPreferenceRepository().deleteByUserId(userId); await createCurrentHostSidebarPreferenceRepository().deleteByUserId(userId); await createCurrentCredentialSidebarPreferenceRepository().deleteByUserId( userId, diff --git a/src/backend/database/routes/host-bulk-routes.ts b/src/backend/database/routes/host-bulk-routes.ts index 2c5ac5f05..c981eaa10 100644 --- a/src/backend/database/routes/host-bulk-routes.ts +++ b/src/backend/database/routes/host-bulk-routes.ts @@ -292,6 +292,8 @@ export function registerHostBulkRoutes( simpleUpdates.enableTerminalToolbar = updates.enableTerminalToolbar; if (typeof updates.enableAiAssistant === "boolean") simpleUpdates.enableAiAssistant = updates.enableAiAssistant; + if (typeof updates.statusCheckEnabled === "boolean") + simpleUpdates.statusCheckEnabled = updates.statusCheckEnabled; if (Object.keys(simpleUpdates).length > 0) { await hostRepository.updateManyForUser( @@ -301,22 +303,6 @@ export function registerHostBulkRoutes( ); } - if (updates.statsConfig && typeof updates.statsConfig === "object") { - for (const host of ownedHosts) { - try { - const existing = host.statsConfig - ? JSON.parse(host.statsConfig as string) - : {}; - const merged = { ...existing, ...updates.statsConfig }; - await hostRepository.updateForUser(userId, host.id, { - statsConfig: JSON.stringify(merged), - }); - } catch { - errors.push(`Failed to update statsConfig for host ${host.id}`); - } - } - } - // Proxmox enable/disable now lives in the proxmox plugin's own // host-scope settings, not a hosts column. Disabling is a plain flag // flip; enabling also defaults the Proxmox credential to the one @@ -784,9 +770,7 @@ export function registerHostBulkRoutes( quickActions: hostData.quickActions ? JSON.stringify(hostData.quickActions) : null, - statsConfig: hostData.statsConfig - ? JSON.stringify(hostData.statsConfig) - : null, + ...importedStatusCheck(hostData as Record), dockerConfig: hostData.dockerConfig ? JSON.stringify(hostData.dockerConfig) : null, @@ -1048,7 +1032,8 @@ export function registerHostBulkRoutes( ? JSON.stringify(hostData.jumpHosts) : null, quickActions: null, - statsConfig: null, + statusCheckEnabled: true, + statusCheckInterval: null, dockerConfig: null, terminalConfig: null, forceKeyboardInteractive: "false", @@ -1099,3 +1084,29 @@ export function registerHostBulkRoutes( }, ); } + +/** + * Status check fields from an import row. Exports from before 2.9.0 carried + * them inside statsConfig. + */ +function importedStatusCheck(raw: Record): { + statusCheckEnabled: boolean; + statusCheckInterval: number | null; +} { + const legacy = + raw.statsConfig && typeof raw.statsConfig === "object" + ? (raw.statsConfig as Record) + : {}; + const enabled = raw.statusCheckEnabled ?? legacy.statusCheckEnabled; + const useGlobal = legacy.useGlobalStatusInterval !== false; + const interval = + raw.statusCheckInterval ?? (useGlobal ? null : legacy.statusCheckInterval); + const seconds = Number(interval); + return { + statusCheckEnabled: enabled !== false && legacy.disableTcpPing !== true, + statusCheckInterval: + interval != null && Number.isInteger(seconds) && seconds >= 5 + ? seconds + : null, + }; +} diff --git a/src/backend/database/routes/host-normalizers.ts b/src/backend/database/routes/host-normalizers.ts index cc6eddb43..f379b073c 100644 --- a/src/backend/database/routes/host-normalizers.ts +++ b/src/backend/database/routes/host-normalizers.ts @@ -191,7 +191,8 @@ export type NormalizedImportedHost = Record & { tunnelConnections?: unknown; jumpHosts?: unknown; quickActions?: unknown; - statsConfig?: unknown; + statusCheckEnabled?: unknown; + statusCheckInterval?: unknown; dockerConfig?: unknown; webUiConfig?: unknown; proxmoxConfig?: unknown; @@ -348,6 +349,8 @@ const CONNECT_LEVEL_FIELDS = new Set([ "showTunnelInSidebar", "showDockerInSidebar", "showServerStatsInSidebar", + "statusCheckEnabled", + "statusCheckInterval", "enableSsh", "sshPort", "rdpAuthType", @@ -456,9 +459,12 @@ export function transformHostResponse( quickActions: host.quickActions ? JSON.parse(host.quickActions as string) : [], - statsConfig: host.statsConfig - ? JSON.parse(host.statsConfig as string) - : undefined, + statusCheckEnabled: + host.statusCheckEnabled !== false && host.statusCheckEnabled !== 0, + statusCheckInterval: + typeof host.statusCheckInterval === "number" + ? host.statusCheckInterval + : null, terminalConfig: host.terminalConfig ? JSON.parse(host.terminalConfig as string) : undefined, diff --git a/src/backend/database/routes/host-status-routes.ts b/src/backend/database/routes/host-status-routes.ts new file mode 100644 index 000000000..4faf49ef6 --- /dev/null +++ b/src/backend/database/routes/host-status-routes.ts @@ -0,0 +1,259 @@ +import type { AuthenticatedRequest } from "../../../types/index.js"; +import type { Request, RequestHandler, Response, Router } from "express"; +import { PermissionManager } from "../../utils/permission-manager.js"; +import { DataCrypto } from "../../utils/data-crypto.js"; +import { sshLogger } from "../../utils/logger.js"; +import { createCurrentSettingsRepository } from "../repositories/factory.js"; +import { + DEFAULT_STATUS_INTERVAL, + GLOBAL_STATUS_INTERVAL_KEY, + hostStatusService, + type HostStatusEntry, +} from "../../hosts/status/host-status-service.js"; + +interface HostStatusRoutesDeps { + authenticateJWT: RequestHandler; + requireAdmin: RequestHandler; +} + +/** "1,2,3" to a set of ids; undefined means the caller asked for all. */ +export function parseStatusHostIds(value: unknown): Set | null { + if (value === undefined) return null; + if (typeof value !== "string") return new Set(); + return new Set( + value + .split(",") + .map(Number) + .filter((id) => Number.isSafeInteger(id) && id > 0), + ); +} + +function sessionExpired(res: Response): Response { + return res.status(401).json({ + error: "Session expired - please log in again", + code: "SESSION_EXPIRED", + }); +} + +export function registerHostStatusRoutes( + router: Router, + { authenticateJWT, requireAdmin }: HostStatusRoutesDeps, +): void { + const permissionManager = PermissionManager.getInstance(); + + /** + * @openapi + * /host/status: + * get: + * summary: Get host statuses + * description: Returns the online, reachable or offline status of every host the user can see. Starts status checks for the user's own hosts on first call. + * tags: + * - SSH + * parameters: + * - in: query + * name: hostIds + * required: false + * schema: + * type: string + * description: Comma separated ids. Only these of the user's own hosts are checked. + * responses: + * 200: + * description: A map of host ids to status entries. + * 401: + * description: Session expired - please log in again. + */ + router.get( + "/status", + authenticateJWT, + async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + if (DataCrypto.getUserDataKey(userId) === null) + return sessionExpired(res); + + const requested = parseStatusHostIds(req.query.hostIds); + const statuses = await hostStatusService.statusesFor(userId, requested); + + // One batched permission check: this is polled every few seconds, and a + // per-host check made it linear in host count against the database. + const entries = [...statuses.entries()]; + const allowed = await permissionManager.filterAccessibleHostIds( + userId, + entries.map(([id]) => id), + ); + + const result: Record = {}; + for (const [id, entry] of entries) { + if (allowed.has(id) && (requested === null || requested.has(id))) { + result[id] = entry; + } + } + res.json(result); + }, + ); + + /** + * @openapi + * /host/status/settings: + * get: + * summary: Get status check settings + * description: Returns the default number of seconds between status checks. + * tags: + * - SSH + * responses: + * 200: + * description: The status check settings. + * 403: + * description: Admin access required. + */ + router.get( + "/status/settings", + authenticateJWT, + requireAdmin, + async (_req: Request, res: Response) => { + const value = Number( + await createCurrentSettingsRepository().get(GLOBAL_STATUS_INTERVAL_KEY), + ); + res.json({ + statusCheckInterval: + Number.isInteger(value) && value >= 5 + ? value + : DEFAULT_STATUS_INTERVAL, + }); + }, + ); + + /** + * @openapi + * /host/status/settings: + * put: + * summary: Update status check settings + * description: Sets the default number of seconds between status checks and re-times every running check. + * tags: + * - SSH + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * statusCheckInterval: + * type: integer + * minimum: 5 + * maximum: 3600 + * responses: + * 200: + * description: Settings saved. + * 400: + * description: Invalid interval. + * 403: + * description: Admin access required. + */ + router.put( + "/status/settings", + authenticateJWT, + requireAdmin, + async (req: Request, res: Response) => { + const seconds = Number(req.body?.statusCheckInterval); + if (!Number.isInteger(seconds) || seconds < 5 || seconds > 3600) { + return res + .status(400) + .json({ error: "Status check interval must be 5 to 3600 seconds" }); + } + try { + await createCurrentSettingsRepository().set( + GLOBAL_STATUS_INTERVAL_KEY, + String(seconds), + ); + hostStatusService.retimeAll(); + res.json({ statusCheckInterval: seconds }); + } catch (error) { + sshLogger.error("Failed to save status check settings", error, { + operation: "host_status_settings", + }); + res.status(500).json({ error: "Failed to save settings" }); + } + }, + ); + + /** + * @openapi + * /host/status/refresh: + * post: + * summary: Restart status checks + * description: Re-reads the user's own hosts and restarts their status checks. + * tags: + * - SSH + * responses: + * 200: + * description: Status checks restarted. + * 401: + * description: Session expired - please log in again. + */ + router.post( + "/status/refresh", + authenticateJWT, + async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + if (DataCrypto.getUserDataKey(userId) === null) { + return sessionExpired(res); + } + await hostStatusService.refresh(userId); + res.json({ message: "Status checks restarted" }); + }, + ); + + /** + * @openapi + * /host/status/{id}: + * get: + * summary: Get one host's status + * description: Returns the status of one host the user can connect to. + * tags: + * - SSH + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: The host's status entry. + * 400: + * description: Invalid host id. + * 404: + * description: Status not available. + */ + router.get( + "/status/:id", + authenticateJWT, + async (req: Request, res: Response) => { + const id = Number(req.params.id); + if (!Number.isInteger(id) || id <= 0) { + return res.status(400).json({ error: "Invalid host ID" }); + } + const userId = (req as AuthenticatedRequest).userId; + if (DataCrypto.getUserDataKey(userId) === null) { + return sessionExpired(res); + } + + const access = await permissionManager.canAccessHost( + userId, + id, + "connect", + ); + if (!access.hasAccess) { + return res.status(404).json({ error: "Status not available" }); + } + + await hostStatusService.statusesFor(userId, null); + const entry = + hostStatusService.get(id) ?? (await hostStatusService.check(id)); + if (!entry) { + return res.status(404).json({ error: "Status not available" }); + } + res.json(entry); + }, + ); +} diff --git a/src/backend/database/routes/host.ts b/src/backend/database/routes/host.ts index 1edad51d3..f550138d3 100644 --- a/src/backend/database/routes/host.ts +++ b/src/backend/database/routes/host.ts @@ -55,6 +55,7 @@ import { registerHostAutostartRoutes } from "./host-autostart-routes.js"; import { registerHostInternalRoutes } from "./host-internal-routes.js"; import { registerHostNetworkRoutes } from "./host-network-routes.js"; import { registerHostBulkRoutes } from "./host-bulk-routes.js"; +import { registerHostStatusRoutes } from "./host-status-routes.js"; import { applyHostEnrollmentDefaults, requireHostEnrollmentAccessForPath, @@ -109,6 +110,11 @@ const requireDataAccess = authManager.createDataAccessMiddleware(); registerHostInternalRoutes(router); +registerHostStatusRoutes(router, { + authenticateJWT, + requireAdmin: authManager.createAdminMiddleware(), +}); + /** * @openapi * /host/db/host: @@ -207,7 +213,8 @@ router.post( tunnelConnections, jumpHosts, quickActions, - statsConfig, + statusCheckEnabled, + statusCheckInterval, dockerConfig, proxmoxConfig, enableProxmoxStats, @@ -335,11 +342,8 @@ router.post( showDockerInSidebar: showDockerInSidebar ? 1 : 0, showServerStatsInSidebar: showServerStatsInSidebar ? 1 : 0, defaultPath: defaultPath || null, - statsConfig: statsConfig - ? typeof statsConfig === "string" - ? statsConfig - : JSON.stringify(statsConfig) - : null, + statusCheckEnabled: statusCheckEnabled === false ? 0 : 1, + statusCheckInterval: normalizeStatusInterval(statusCheckInterval), dockerConfig: dockerConfig ? typeof dockerConfig === "string" ? dockerConfig @@ -774,7 +778,8 @@ router.post( tunnelConnections: [], jumpHosts: [], quickActions: [], - statsConfig: {}, + statusCheckEnabled: true, + statusCheckInterval: null, notes: "", createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), @@ -904,7 +909,8 @@ router.put( tunnelConnections, jumpHosts, quickActions, - statsConfig, + statusCheckEnabled, + statusCheckInterval, dockerConfig, proxmoxConfig, enableProxmoxStats, @@ -1033,11 +1039,8 @@ router.put( showDockerInSidebar: showDockerInSidebar ? 1 : 0, showServerStatsInSidebar: showServerStatsInSidebar ? 1 : 0, defaultPath: defaultPath || null, - statsConfig: statsConfig - ? typeof statsConfig === "string" - ? statsConfig - : JSON.stringify(statsConfig) - : null, + statusCheckEnabled: statusCheckEnabled === false ? 0 : 1, + statusCheckInterval: normalizeStatusInterval(statusCheckInterval), dockerConfig: dockerConfig ? typeof dockerConfig === "string" ? dockerConfig @@ -2119,9 +2122,6 @@ router.get( quickActions: resolvedHost.quickActions ? JSON.parse(resolvedHost.quickActions as string) : null, - statsConfig: resolvedHost.statsConfig - ? JSON.parse(resolvedHost.statsConfig as string) - : null, webUiConfig: webEndpointSettings?.webUiConfig ?? { endpoints: [] }, dockerConfig: resolvedHost.dockerConfig ? JSON.parse(resolvedHost.dockerConfig as string) @@ -2285,9 +2285,6 @@ router.get( quickActions: resolvedHost.quickActions ? JSON.parse(resolvedHost.quickActions as string) : null, - statsConfig: resolvedHost.statsConfig - ? JSON.parse(resolvedHost.statsConfig as string) - : null, webUiConfig: webEndpointSettings?.webUiConfig ?? { endpoints: [], }, @@ -2929,3 +2926,12 @@ function safeParseJson(raw: string): unknown { return null; } } + +/** Seconds between status checks, or null to follow the global setting. */ +function normalizeStatusInterval(value: unknown): number | null { + const seconds = Number(value); + if (value === null || value === undefined || value === "") return null; + return Number.isInteger(seconds) && seconds >= 5 && seconds <= 86400 + ? seconds + : null; +} diff --git a/src/backend/database/routes/user-settings-routes.ts b/src/backend/database/routes/user-settings-routes.ts index a7b7a3ece..33dbbca17 100644 --- a/src/backend/database/routes/user-settings-routes.ts +++ b/src/backend/database/routes/user-settings-routes.ts @@ -32,7 +32,6 @@ export type HostDefaults = { socks5Username?: string; socks5Password?: string; credentialId?: number | null; - metricsEnabled?: boolean; statusCheckEnabled?: boolean; fontSize?: number; fontFamily?: string; diff --git a/src/backend/hosts/host-key-verifier.ts b/src/backend/hosts/host-key-verifier.ts index db77263a3..fb7158f25 100644 --- a/src/backend/hosts/host-key-verifier.ts +++ b/src/backend/hosts/host-key-verifier.ts @@ -1,4 +1,4 @@ -import { authFailureTracker } from "./metrics-shared/state.js"; +import { pluginEvents, TOPICS } from "../plugins/events.js"; import type { WebSocket } from "ws"; import { createCurrentHostResolutionRepository } from "../database/repositories/factory.js"; import { sshLogger } from "../utils/logger.js"; @@ -328,7 +328,7 @@ export class SSHHostKeyVerifier { algorithm, currentChangeCount, ); - authFailureTracker.resetHostKeyFailure(hostId); + pluginEvents.emit(TOPICS.hostKeyUpdated, { hostId }); } private static async promptUserForNewKey( diff --git a/src/backend/hosts/host-resolver.ts b/src/backend/hosts/host-resolver.ts index 5a48c7989..731ab819e 100644 --- a/src/backend/hosts/host-resolver.ts +++ b/src/backend/hosts/host-resolver.ts @@ -117,13 +117,6 @@ export async function resolveHostById( host.tunnelConnections = []; } } - if (typeof host.statsConfig === "string" && host.statsConfig) { - try { - host.statsConfig = JSON.parse(host.statsConfig as string); - } catch { - host.statsConfig = undefined; - } - } if (typeof host.terminalConfig === "string" && host.terminalConfig) { try { host.terminalConfig = JSON.parse(host.terminalConfig as string); diff --git a/src/backend/hosts/metrics-shared/common-utils.ts b/src/backend/hosts/metrics-shared/common-utils.ts deleted file mode 100644 index 1d536fe57..000000000 --- a/src/backend/hosts/metrics-shared/common-utils.ts +++ /dev/null @@ -1,145 +0,0 @@ -import type { Client, ClientChannel } from "ssh2"; - -export function execCommand( - client: Client, - command: string, - timeoutMs = 30000, -): Promise<{ - stdout: string; - stderr: string; - code: number | null; -}> { - return new Promise((resolve, reject) => { - let settled = false; - let stream: ClientChannel | null = null; - - const timeout = setTimeout(() => { - if (!settled) { - settled = true; - cleanup(); - reject(new Error(`Command timeout after ${timeoutMs}ms: ${command}`)); - } - }, timeoutMs); - - const cleanup = () => { - clearTimeout(timeout); - if (stream) { - try { - stream.removeAllListeners(); - if (stream.stderr) { - stream.stderr.removeAllListeners(); - } - stream.destroy(); - } catch { - // expected - cleanup errors ignored - } - } - }; - - client.exec(command, { pty: false }, (err, _stream) => { - if (err) { - if (!settled) { - settled = true; - cleanup(); - reject(err); - } - return; - } - - stream = _stream; - let stdout = ""; - let stderr = ""; - let exitCode: number | null = null; - - stream - .on("close", (code: number | undefined) => { - if (!settled) { - settled = true; - exitCode = typeof code === "number" ? code : null; - cleanup(); - resolve({ stdout, stderr, code: exitCode }); - } - }) - .on("data", (data: Buffer) => { - stdout += data.toString("utf8"); - }) - .on("error", (streamErr: Error) => { - if (!settled) { - settled = true; - cleanup(); - reject(streamErr); - } - }); - - if (stream.stderr) { - stream.stderr - .on("data", (data: Buffer) => { - stderr += data.toString("utf8"); - }) - .on("error", (stderrErr: Error) => { - if (!settled) { - settled = true; - cleanup(); - reject(stderrErr); - } - }); - } - }); - }); -} - -export type HostPlatform = "darwin" | "linux" | "windows" | "other"; - -// Windows OpenSSH's default shell is usually cmd.exe (occasionally -// PowerShell), so scripts are sent base64-encoded via -EncodedCommand - -// that avoids every cmd.exe quoting/escaping pitfall entirely, since the -// argument ends up being plain alphanumeric text with no shell metacharacters. -export function encodePowerShellCommand(script: string): string { - return Buffer.from(script, "utf16le").toString("base64"); -} - -export function execPowerShell( - client: Client, - script: string, - timeoutMs = 30000, -): Promise<{ stdout: string; stderr: string; code: number | null }> { - const encoded = encodePowerShellCommand(script); - return execCommand( - client, - `powershell -NoProfile -NonInteractive -EncodedCommand ${encoded}`, - timeoutMs, - ); -} - -export async function detectPlatform(client: Client): Promise { - try { - const { stdout, code } = await execCommand(client, "uname -s", 10000); - const kernel = stdout.trim().toLowerCase(); - if (kernel === "darwin") return "darwin"; - if (kernel === "linux") return "linux"; - if (code === 0 && kernel) return "other"; - } catch { - // expected on hosts with no POSIX shell (e.g. Windows via cmd.exe) - } - - try { - const { stdout } = await execPowerShell(client, "'WIN_OK'", 10000); - if (stdout.includes("WIN_OK")) return "windows"; - } catch { - // expected on hosts with no PowerShell available - } - - return "other"; -} - -export function toFixedNum( - n: number | null | undefined, - digits = 2, -): number | null { - if (typeof n !== "number" || !Number.isFinite(n)) return null; - return Number(n.toFixed(digits)); -} - -export function kibToGiB(kib: number): number { - return kib / (1024 * 1024); -} diff --git a/src/backend/hosts/metrics-shared/exec-elevated.ts b/src/backend/hosts/metrics-shared/exec-elevated.ts deleted file mode 100644 index 7bbde7870..000000000 --- a/src/backend/hosts/metrics-shared/exec-elevated.ts +++ /dev/null @@ -1,169 +0,0 @@ -import type { Client } from "ssh2"; -import { execCommand } from "./common-utils.js"; - -export type ElevationErrorCode = "SUDO_REQUIRED" | "SUDO_FAILED" | "NOT_SUDOER"; - -export class ElevationError extends Error { - code: ElevationErrorCode; - constructor(code: ElevationErrorCode, message: string) { - super(message); - this.name = "ElevationError"; - this.code = code; - } -} - -export interface ElevatedResult { - stdout: string; - stderr: string; - code: number | null; - usedSudo: boolean; -} - -const PERMISSION_DENIED = [ - "permission denied", - "operation not permitted", - "must be run as root", - "must be superuser", - "you need to be root", - "are you root", - "access denied", -]; - -/** - * Phrases sudo itself prints to STDERR when authentication or authorization - * fails. We only match these against sudo's stderr (never command output), so a - * command that happens to print "permission denied" on stdout is not mistaken - * for a sudo failure. - */ -const SUDO_AUTH_FAILED = [ - "incorrect password", - "a password is required", - "a terminal is required", - "no tty present", - "sorry, try again", - "no password was provided", - "1 incorrect password attempt", -]; - -const SUDO_NOT_SUDOER = [ - "is not in the sudoers file", - "not allowed to run sudo", - "not allowed to execute", -]; - -/** - * Marker printed only after sudo has successfully authenticated and started the - * inner shell. Its presence on stdout is the authoritative "elevation worked" - * signal; its absence (together with sudo stderr) means auth failed. - */ -const SUDO_OK_MARKER = "__TX_SUDO_OK__"; -const SUDO_OK_LINE_RE = new RegExp(`^${SUDO_OK_MARKER}\\r?\\n?`); - -/** Escape a value for single-quoted shell context. */ -export function shellSingleQuote(value: string): string { - return `'${value.replace(/'/g, "'\"'\"'")}'`; -} - -/** - * Build the elevated command string: - * echo '' | sudo -S -p '' sh -c 'echo __TX_SUDO_OK__; ' - * - * `-p ''` suppresses the prompt. stderr is NOT merged into stdout, so the - * command's own output stays clean and sudo's auth errors stay on stderr. The - * marker is echoed by the inner shell once sudo authenticates, letting us tell a - * genuine auth failure from command output that merely contains scary words. - */ -export function buildSudoCommand( - command: string, - sudoPassword: string, -): string { - const pw = shellSingleQuote(sudoPassword); - const inner = shellSingleQuote(`echo ${SUDO_OK_MARKER}; ${command}`); - return `echo ${pw} | sudo -S -p '' sh -c ${inner}`; -} - -function includesAny(text: string, needles: string[]): boolean { - const lower = text.toLowerCase(); - return needles.some((n) => lower.includes(n)); -} - -function looksLikePermissionDenied(text: string): boolean { - return includesAny(text, PERMISSION_DENIED); -} - -/** Strip the success marker line from the front of stdout. */ -function stripMarker(stdout: string): string { - return stdout.replace(SUDO_OK_LINE_RE, ""); -} - -/** - * Run a command on a pooled client, elevating with the host's stored sudo - * password only when needed (or when forced). Throws a typed `ElevationError` - * when elevation is required but unavailable/incorrect. - */ -export async function execElevated( - client: Client, - command: string, - sudoPassword: string | undefined, - opts: { forceSudo?: boolean; timeoutMs?: number } = {}, -): Promise { - const timeoutMs = opts.timeoutMs ?? 30000; - - if (!opts.forceSudo) { - const direct = await execCommand(client, command, timeoutMs); - if (direct.code === 0) { - return { ...direct, usedSudo: false }; - } - // Only escalate when the failure looks like a privilege problem. A - // permission-denied phrase on stderr (not arbitrary stdout) is the signal. - if (!looksLikePermissionDenied(direct.stderr)) { - return { ...direct, usedSudo: false }; - } - if (!sudoPassword) { - throw new ElevationError( - "SUDO_REQUIRED", - "This action requires elevated privileges. Set a sudo password for this host to continue.", - ); - } - } else if (!sudoPassword) { - throw new ElevationError( - "SUDO_REQUIRED", - "This action requires elevated privileges. Set a sudo password for this host to continue.", - ); - } - - const sudoCmd = buildSudoCommand(command, sudoPassword as string); - const result = await execCommand(client, sudoCmd, timeoutMs); - const authenticated = result.stdout.includes(SUDO_OK_MARKER); - - if (!authenticated) { - // Elevation never started: diagnose from sudo's stderr only. - const notSudoer = includesAny(result.stderr, SUDO_NOT_SUDOER); - const authFailed = includesAny(result.stderr, SUDO_AUTH_FAILED); - if (notSudoer) { - throw new ElevationError( - "NOT_SUDOER", - "The connected user is not permitted to use sudo on this host.", - ); - } - if (authFailed) { - throw new ElevationError( - "SUDO_FAILED", - "Elevation failed. Check the host's sudo password.", - ); - } - // No marker and no recognizable sudo error: treat as a generic failure but - // keep the original output so the caller can surface it. - throw new ElevationError( - "SUDO_FAILED", - "Elevation failed. Check the host's sudo password.", - ); - } - - return { - stdout: stripMarker(result.stdout), - stderr: result.stderr, - code: result.code, - usedSudo: true, - }; -} diff --git a/src/backend/hosts/metrics-shared/package-commands.ts b/src/backend/hosts/metrics-shared/package-commands.ts deleted file mode 100644 index 37d933728..000000000 --- a/src/backend/hosts/metrics-shared/package-commands.ts +++ /dev/null @@ -1,84 +0,0 @@ -import type { PackageManager } from "./platform.js"; - -export interface UpgradablePackage { - name: string; - currentVersion?: string; - newVersion?: string; -} - -export function buildListUpgradableCommand(pkg: PackageManager): string | null { - switch (pkg) { - case "apt": - return "apt list --upgradable 2>/dev/null | tail -n +2"; - case "dnf": - return "dnf -q check-update 2>/dev/null || true"; - case "yum": - return "yum -q check-update 2>/dev/null || true"; - case "pacman": - return "pacman -Qu 2>/dev/null || true"; - default: - return null; - } -} - -export function parseUpgradable( - pkg: PackageManager, - output: string, -): UpgradablePackage[] { - const out: UpgradablePackage[] = []; - const lines = output - .split("\n") - .map((l) => l.trim()) - .filter(Boolean); - if (pkg === "apt") { - for (const line of lines) { - // name/suite newver arch [upgradable from: oldver] - const m = line.match( - /^([^/\s]+)\/\S+\s+(\S+)\s+\S+(?:\s+\[upgradable from:\s+(\S+)\])?/, - ); - if (m) out.push({ name: m[1], newVersion: m[2], currentVersion: m[3] }); - } - } else if (pkg === "dnf" || pkg === "yum") { - for (const line of lines) { - if (/^(Last metadata|Obsoleting|Security:)/i.test(line)) continue; - const m = line.match(/^(\S+)\s+(\S+)\s+\S+$/); - if (m && m[1].includes(".")) out.push({ name: m[1], newVersion: m[2] }); - } - } else if (pkg === "pacman") { - for (const line of lines) { - const m = line.match(/^(\S+)\s+(\S+)\s+->\s+(\S+)$/); - if (m) out.push({ name: m[1], currentVersion: m[2], newVersion: m[3] }); - } - } - return out; -} - -export type PackageAction = "upgrade-all" | "install" | "upgrade"; - -export function buildPackageActionCommand( - pkg: PackageManager, - action: PackageAction, - name?: string, -): string | null { - const target = name ? ` ${name}` : ""; - switch (pkg) { - case "apt": - if (action === "upgrade-all") - return "DEBIAN_FRONTEND=noninteractive apt-get -y upgrade"; - return `DEBIAN_FRONTEND=noninteractive apt-get -y install${target}`; - case "dnf": - return action === "upgrade-all" - ? "dnf -y upgrade" - : `dnf -y install${target}`; - case "yum": - return action === "upgrade-all" - ? "yum -y update" - : `yum -y install${target}`; - case "pacman": - return action === "upgrade-all" - ? "pacman -Syu --noconfirm" - : `pacman -S --noconfirm${target}`; - default: - return null; - } -} diff --git a/src/backend/hosts/metrics-shared/platform.ts b/src/backend/hosts/metrics-shared/platform.ts deleted file mode 100644 index 37e109eed..000000000 --- a/src/backend/hosts/metrics-shared/platform.ts +++ /dev/null @@ -1,75 +0,0 @@ -import type { Client } from "ssh2"; -import { execCommand } from "./common-utils.js"; - -export type PackageManager = "apt" | "dnf" | "yum" | "pacman" | null; - -export interface PlatformInfo { - hasSystemd: boolean; - pkg: PackageManager; - hasCertbot: boolean; - hasAcmeSh: boolean; - hasDocker: boolean; - osPrettyName: string | null; -} - -/** - * Non-interactive SSH shells don't source ~/.zprofile or ~/.bash_profile, - * so PATH additions from installers like Homebrew or OrbStack are missing. - * Extend PATH with their common install locations before probing. - */ -export const EXTRA_PATH_DIRS = - '/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:"$HOME/.orbstack/bin"'; - -const DOCKER_PROBE = - `command -v docker >/dev/null 2>&1` + - ` || [ -S /var/run/docker.sock ]` + - ` || [ -S "$HOME/.orbstack/run/docker.sock" ]`; - -/** - * Single probe that reports which tooling is available. Each line is - * "key=value" so the parser is trivial and order-independent. - */ -export const PLATFORM_PROBE_COMMAND = [ - `PATH="${EXTRA_PATH_DIRS}:$PATH"`, - "echo systemd=$(command -v systemctl >/dev/null 2>&1 && echo 1 || echo 0)", - "echo apt=$(command -v apt-get >/dev/null 2>&1 && echo 1 || echo 0)", - "echo dnf=$(command -v dnf >/dev/null 2>&1 && echo 1 || echo 0)", - "echo yum=$(command -v yum >/dev/null 2>&1 && echo 1 || echo 0)", - "echo pacman=$(command -v pacman >/dev/null 2>&1 && echo 1 || echo 0)", - "echo certbot=$(command -v certbot >/dev/null 2>&1 && echo 1 || echo 0)", - 'echo acmesh=$( { command -v acme.sh >/dev/null 2>&1 || [ -x "$HOME/.acme.sh/acme.sh" ]; } && echo 1 || echo 0)', - `echo docker=$(${DOCKER_PROBE} && echo 1 || echo 0)`, - 'echo os=$(. /etc/os-release 2>/dev/null && echo "$PRETTY_NAME")', -].join("; "); - -export function parsePlatformProbe(output: string): PlatformInfo { - const map = new Map(); - for (const line of output.split("\n")) { - const idx = line.indexOf("="); - if (idx === -1) continue; - map.set(line.slice(0, idx).trim(), line.slice(idx + 1).trim()); - } - const on = (k: string) => map.get(k) === "1"; - - // Prefer dnf over yum when both exist (dnf is the modern front-end). - let pkg: PackageManager = null; - if (on("apt")) pkg = "apt"; - else if (on("dnf")) pkg = "dnf"; - else if (on("yum")) pkg = "yum"; - else if (on("pacman")) pkg = "pacman"; - - const os = map.get("os"); - return { - hasSystemd: on("systemd"), - pkg, - hasCertbot: on("certbot"), - hasAcmeSh: on("acmesh"), - hasDocker: on("docker"), - osPrettyName: os && os.length > 0 ? os : null, - }; -} - -export async function detectPlatform(client: Client): Promise { - const { stdout } = await execCommand(client, PLATFORM_PROBE_COMMAND, 15000); - return parsePlatformProbe(stdout); -} diff --git a/src/backend/hosts/status/host-status-service.ts b/src/backend/hosts/status/host-status-service.ts new file mode 100644 index 000000000..81ff96681 --- /dev/null +++ b/src/backend/hosts/status/host-status-service.ts @@ -0,0 +1,458 @@ +/** + * The host list's status dot. + * + * Every host with status checks on gets a TCP connection to its port on a + * timer. That alone says "reachable". A host becomes "online" when a login + * worked: an open terminal session, or a plugin reporting one through + * ctx.hosts.status.reportLogin. Probing never logs in: on RADIUS or Duo backed + * devices a login fires a real 2FA push, every interval. + * + * Polling is demand driven. A user asking for statuses starts polling their + * own hosts; nothing is probed for a user who never opened the app. + */ + +import { pluginEvents, TOPICS } from "../../plugins/events.js"; +import { hostSessionStatus } from "../host-session-status.js"; +import { sshLogger } from "../../utils/logger.js"; +import { + createCurrentHostResolutionRepository, + getCurrentSettingValue, +} from "../../database/repositories/factory.js"; +import type { HostStatusTargetRow } from "../../database/repositories/host-resolution-repository.js"; +import { + statusAfterAuthentication, + statusAfterReachabilityCheck, + type HostStatus, +} from "./host-status.js"; +import { ConcurrentLimiter } from "./limiter.js"; +import { tcpPing, tcpPingThroughJumpHost } from "./tcp-ping.js"; + +export const GLOBAL_STATUS_INTERVAL_KEY = "global_status_check_interval"; +export const DEFAULT_STATUS_INTERVAL = 60; +/** A status younger than this is fresh enough for check(). */ +const FRESH_MS = 30_000; + +export interface HostStatusEntry { + status: HostStatus; + lastChecked: string; + reason?: "host_key_changed"; +} + +export interface HostStatusPayload { + hostId: number; + ownerUserId: string; + status: HostStatus; + previous: HostStatus | null; + /** Anything but offline, the shape automations' trigger already reads. */ + online: boolean; + reason?: "host_key_changed"; +} + +export interface StatusTarget { + id: number; + userId: string; + ip: string; + port: number; + connectionType: string; + jumpHosts: Array<{ hostId: number }>; + statusCheckEnabled: boolean; + statusCheckInterval: number | null; +} + +type PortResolver = ( + hostId: number, +) => number | undefined | Promise; + +export interface HostStatusDeps { + loadTargets: (filter: { + userId?: string; + hostIds?: number[]; + }) => Promise; + ping: (host: string, port: number) => Promise; + pingThroughJumpHosts: ( + target: StatusTarget, + port: number, + ) => Promise; + globalInterval: () => number; + emit: (payload: HostStatusPayload) => void; +} + +function parseJumpHosts(raw: string | null): Array<{ hostId: number }> { + if (!raw) return []; + try { + const parsed = JSON.parse(raw); + return Array.isArray(parsed) + ? parsed.filter( + (hop): hop is { hostId: number } => + !!hop && Number.isInteger((hop as { hostId?: unknown }).hostId), + ) + : []; + } catch { + return []; + } +} + +export function toStatusTarget(row: HostStatusTargetRow): StatusTarget { + return { + id: row.id, + userId: row.userId, + ip: row.ip, + port: row.port, + connectionType: row.connectionType || "ssh", + jumpHosts: parseJumpHosts(row.jumpHosts), + statusCheckEnabled: row.statusCheckEnabled !== false, + statusCheckInterval: row.statusCheckInterval ?? null, + }; +} + +const defaultDeps: HostStatusDeps = { + loadTargets: async (filter) => + ( + await createCurrentHostResolutionRepository().listStatusTargets(filter) + ).map(toStatusTarget), + ping: (host, port) => tcpPing(host, port, 5000), + pingThroughJumpHosts: async (target, port) => { + const { createJumpHostChain } = await import("../jump-host-chain.js"); + const client = await createJumpHostChain( + target.jumpHosts, + target.userId, + ).catch(() => null); + return client + ? tcpPingThroughJumpHost(client, target.ip, port, 5000) + : false; + }, + globalInterval: () => { + const value = Number(getCurrentSettingValue(GLOBAL_STATUS_INTERVAL_KEY)); + return Number.isInteger(value) && value >= 5 + ? value + : DEFAULT_STATUS_INTERVAL; + }, + emit: (payload) => pluginEvents.emit(TOPICS.hostStatus, payload), +}; + +interface Polled { + target: StatusTarget; + timer?: NodeJS.Timeout; +} + +export class HostStatusService { + private readonly polled = new Map(); + private readonly store = new Map(); + private readonly owners = new Map(); + private readonly inFlight = new Map>(); + private readonly sessionOnline = new Set(); + private readonly loggedIn = new Set(); + private readonly startedUsers = new Set(); + private readonly portResolvers = new Map>(); + private readonly limiter = new ConcurrentLimiter(20); + private unsubscribers: Array<() => void> = []; + + constructor(private readonly deps: HostStatusDeps = defaultDeps) {} + + /** Subscribes to the core events the status depends on. */ + start(): void { + if (this.unsubscribers.length > 0) return; + this.unsubscribers = [ + hostSessionStatus.subscribe((hostId, online) => + this.setSessionOnline(hostId, online), + ), + pluginEvents.on(TOPICS.hostUpdated, (payload) => { + const { hostId } = payload as { hostId?: number }; + if (hostId) void this.reload(hostId); + }), + pluginEvents.on(TOPICS.hostDeleted, (payload) => { + const { hostId } = payload as { hostId?: number }; + if (hostId) this.forget(hostId); + }), + pluginEvents.on(TOPICS.hostKeyUpdated, (payload) => { + const { hostId } = payload as { hostId?: number }; + if (hostId) this.clearHostKeyReason(hostId); + }), + ]; + } + + stop(): void { + for (const unsubscribe of this.unsubscribers) unsubscribe(); + this.unsubscribers = []; + for (const hostId of [...this.polled.keys()]) this.stopPolling(hostId); + this.startedUsers.clear(); + } + + get(hostId: number): HostStatusEntry | null { + return this.store.get(hostId) ?? null; + } + + /** + * Statuses for a user's request. With hostIds, polling is brought in line + * with exactly those of their own hosts (the desktop app sends only the + * hosts that live on this backend). Without, their own hosts are started + * once. + */ + async statusesFor( + userId: string, + requestedHostIds: Set | null, + ): Promise> { + if (requestedHostIds !== null) { + await this.reconcile(userId, requestedHostIds); + } else if (!this.startedUsers.has(userId)) { + await this.startUser(userId); + } + return this.store; + } + + /** Drops every timer for a user's hosts and starts them again. */ + async refresh(userId: string): Promise { + const targets = await this.deps.loadTargets({ userId }); + for (const target of targets) this.stopPolling(target.id); + for (const target of targets) this.startPolling(target); + this.startedUsers.add(userId); + } + + /** Re-times every polled host, after the global interval changed. */ + retimeAll(): void { + for (const { target } of [...this.polled.values()]) { + this.startPolling(target, false); + } + } + + /** Checks now unless the last result is fresh. */ + async check(hostId: number): Promise { + const current = this.store.get(hostId); + if (current && Date.now() - Date.parse(current.lastChecked) < FRESH_MS) { + return current; + } + const target = + this.polled.get(hostId)?.target ?? + (await this.deps.loadTargets({ hostIds: [hostId] }))[0]; + if (!target || !target.statusCheckEnabled) return current ?? null; + await this.probe(target); + return this.store.get(hostId) ?? null; + } + + reportLogin( + hostId: number, + outcome: { ok: boolean; hostKeyChanged?: boolean }, + ): void { + if (outcome.ok) { + this.loggedIn.add(hostId); + this.set(hostId, { status: statusAfterAuthentication(true) }); + return; + } + this.loggedIn.delete(hostId); + this.set(hostId, { + status: this.sessionOnline.has(hostId) + ? "online" + : statusAfterAuthentication(false, this.store.get(hostId)?.status), + ...(outcome.hostKeyChanged ? { reason: "host_key_changed" } : {}), + }); + } + + registerPort(connectionType: string, resolve: PortResolver): () => void { + let set = this.portResolvers.get(connectionType); + if (!set) { + set = new Set(); + this.portResolvers.set(connectionType, set); + } + set.add(resolve); + return () => { + set!.delete(resolve); + if (set!.size === 0) this.portResolvers.delete(connectionType); + }; + } + + private async startUser(userId: string): Promise { + this.startedUsers.add(userId); + const targets = await this.deps.loadTargets({ userId }); + for (const target of targets) { + if (!this.polled.has(target.id)) this.startPolling(target); + } + } + + private async reconcile(userId: string, allowed: Set): Promise { + const targets = await this.deps.loadTargets({ userId }); + for (const target of targets) { + if (!allowed.has(target.id)) { + this.stopPolling(target.id); + this.store.delete(target.id); + continue; + } + if (!this.polled.has(target.id)) this.startPolling(target); + } + } + + private async reload(hostId: number): Promise { + try { + const [target] = await this.deps.loadTargets({ hostIds: [hostId] }); + if (!target) { + this.forget(hostId); + return; + } + if (this.polled.has(hostId) || this.startedUsers.has(target.userId)) { + this.startPolling(target); + } + } catch (error) { + sshLogger.warn("Could not reload host for status checks", { + operation: "host_status_reload", + hostId, + error: error instanceof Error ? error.message : String(error), + }); + } + } + + private forget(hostId: number): void { + this.stopPolling(hostId); + this.store.delete(hostId); + this.owners.delete(hostId); + this.loggedIn.delete(hostId); + } + + private clearHostKeyReason(hostId: number): void { + const current = this.store.get(hostId); + if (current?.reason !== "host_key_changed") return; + this.store.set(hostId, { + status: current.status, + lastChecked: current.lastChecked, + }); + } + + private setSessionOnline(hostId: number, online: boolean): void { + if (online) { + this.sessionOnline.add(hostId); + if (this.polled.has(hostId)) this.set(hostId, { status: "online" }); + return; + } + this.sessionOnline.delete(hostId); + if ( + !this.loggedIn.has(hostId) && + this.store.get(hostId)?.status === "online" + ) { + this.set(hostId, { status: "reachable" }); + } + } + + private intervalMs(target: StatusTarget): number { + const seconds = target.statusCheckInterval ?? this.deps.globalInterval(); + const intervalMs = Math.max(5, seconds) * 1000; + // Spread hosts so a fleet does not fire on the same second. + const spread = Math.min(intervalMs * 0.2, 15_000); + return ( + intervalMs + ((target.id * 1103515245) % Math.max(1, Math.floor(spread))) + ); + } + + private startPolling(target: StatusTarget, probeNow = true): void { + this.stopPolling(target.id); + this.owners.set(target.id, target.userId); + if (!target.statusCheckEnabled) { + this.store.delete(target.id); + return; + } + const polled: Polled = { target }; + this.polled.set(target.id, polled); + if (probeNow) void this.probe(target); + polled.timer = setInterval(() => { + const latest = this.polled.get(target.id); + if (latest) void this.probe(latest.target); + }, this.intervalMs(target)); + polled.timer.unref?.(); + } + + private stopPolling(hostId: number): void { + const polled = this.polled.get(hostId); + if (polled?.timer) clearInterval(polled.timer); + this.polled.delete(hostId); + } + + private probe(target: StatusTarget): Promise { + const running = this.inFlight.get(target.id); + if (running) return running; + const job = this.limiter + .run(() => this.probeNow(target)) + .catch((error) => { + sshLogger.error("Status check failed", error, { + operation: "host_status_probe", + hostId: target.id, + }); + }) + .finally(() => this.inFlight.delete(target.id)); + this.inFlight.set(target.id, job); + return job; + } + + private async portFor(target: StatusTarget): Promise { + if (target.connectionType !== "ssh") { + for (const resolve of this.portResolvers.get(target.connectionType) ?? + []) { + try { + const port = await resolve(target.id); + if (Number.isInteger(port) && port! > 0) return port!; + } catch { + // fall through to the next resolver + } + } + } + return target.port; + } + + private async probeNow(target: StatusTarget): Promise { + this.owners.set(target.id, target.userId); + let reachable = false; + try { + const port = await this.portFor(target); + reachable = + target.jumpHosts.length > 0 + ? await this.deps.pingThroughJumpHosts(target, port) + : await this.deps.ping(target.ip, port); + } catch { + reachable = false; + } + + const current = this.store.get(target.id); + this.set(target.id, { + status: + reachable && this.sessionOnline.has(target.id) + ? "online" + : statusAfterReachabilityCheck(reachable, current?.status), + ...(reachable && current?.reason === "host_key_changed" + ? { reason: "host_key_changed" as const } + : {}), + }); + } + + private set(hostId: number, entry: Omit) { + const previous = this.store.get(hostId)?.status ?? null; + const next: HostStatusEntry = { + ...entry, + lastChecked: new Date().toISOString(), + }; + this.store.set(hostId, next); + if (previous === next.status) return; + void this.emitChange(hostId, next, previous); + } + + private async emitChange( + hostId: number, + entry: HostStatusEntry, + previous: HostStatus | null, + ): Promise { + let ownerUserId = this.owners.get(hostId); + if (!ownerUserId) { + const [target] = await this.deps + .loadTargets({ hostIds: [hostId] }) + .catch(() => []); + if (!target) return; + ownerUserId = target.userId; + this.owners.set(hostId, ownerUserId); + } + this.deps.emit({ + hostId, + ownerUserId, + status: entry.status, + previous, + online: entry.status !== "offline", + ...(entry.reason ? { reason: entry.reason } : {}), + }); + } +} + +export const hostStatusService = new HostStatusService(); diff --git a/src/backend/hosts/metrics-shared/host-status.ts b/src/backend/hosts/status/host-status.ts similarity index 100% rename from src/backend/hosts/metrics-shared/host-status.ts rename to src/backend/hosts/status/host-status.ts diff --git a/src/backend/hosts/status/limiter.ts b/src/backend/hosts/status/limiter.ts new file mode 100644 index 000000000..c1d1321b8 --- /dev/null +++ b/src/backend/hosts/status/limiter.ts @@ -0,0 +1,81 @@ +/** + * Limits how many async jobs run at once. Extra callers wait in FIFO order. + */ +export class ConcurrentLimiter { + private active = 0; + private readonly waiters: Array<() => void> = []; + + constructor(private maxConcurrent: number) { + if (maxConcurrent < 1) { + throw new Error("maxConcurrent must be >= 1"); + } + } + + get activeCount(): number { + return this.active; + } + + get pendingCount(): number { + return this.waiters.length; + } + + get limit(): number { + return this.maxConcurrent; + } + + /** + * Changes the ceiling at runtime. + * + * Raising it wakes the waiters the new headroom allows, so a queue that built + * up under the old limit drains at once instead of one job at a time as + * running work finishes. Lowering it never interrupts work already running; + * the new limit simply applies from the next release onward. + */ + setLimit(maxConcurrent: number): void { + if (maxConcurrent < 1) { + throw new Error("maxConcurrent must be >= 1"); + } + this.maxConcurrent = maxConcurrent; + this.releaseWaiters(); + } + + /** + * Wakes as many queued callers as there is now room for. + * + * `woken` counts callers that have been resumed but have not yet reached the + * `active += 1` on the far side of their await. Without it the occupancy + * looks lower than it really is for a microtask, and the loop would release + * past the ceiling. + */ + private releaseWaiters(): void { + while ( + this.active + this.woken < this.maxConcurrent && + this.waiters.length > 0 + ) { + this.woken += 1; + this.waiters.shift()!(); + } + } + + private woken = 0; + + async run(fn: () => Promise): Promise { + // Queue behind anything already woken, so a caller arriving mid-handoff + // cannot jump the queue and push occupancy past the ceiling. + if (this.active + this.woken >= this.maxConcurrent) { + await new Promise((resolve) => { + this.waiters.push(resolve); + }); + // Resumed by a slot handoff; that reservation is now consumed. + this.woken = Math.max(0, this.woken - 1); + } + + this.active += 1; + try { + return await fn(); + } finally { + this.active -= 1; + this.releaseWaiters(); + } + } +} diff --git a/src/backend/hosts/status/tcp-ping.ts b/src/backend/hosts/status/tcp-ping.ts new file mode 100644 index 000000000..f060a798e --- /dev/null +++ b/src/backend/hosts/status/tcp-ping.ts @@ -0,0 +1,92 @@ +import net from "net"; +import type { Client } from "ssh2"; + +/** + * Opens a TCP connection and closes it again. An SSH server gets a polite + * banner back so it does not log a failed handshake. + */ +export function tcpPing( + host: string, + port: number, + timeoutMs = 5000, +): Promise { + return new Promise((resolve) => { + const socket = new net.Socket(); + let settled = false; + + const finish = (result: boolean) => { + if (settled) return; + settled = true; + resolve(result); + }; + + const cleanup = () => { + try { + socket.destroy(); + } catch { + // expected + } + }; + + socket.setTimeout(timeoutMs); + + socket.once("connect", () => { + const dataTimeout = setTimeout(() => { + cleanup(); + finish(true); + }, 2000); + + socket.once("data", (data) => { + clearTimeout(dataTimeout); + if (data.toString("utf8").startsWith("SSH-")) { + try { + socket.end("SSH-2.0-TermixHealthCheck\r\n"); + } catch { + // expected + } + setTimeout(cleanup, 200); + } else { + cleanup(); + } + finish(true); + }); + }); + + socket.once("timeout", () => { + cleanup(); + finish(false); + }); + socket.once("error", () => { + cleanup(); + finish(false); + }); + socket.connect(port, host); + }); +} + +/** The same check from the far end of a jump host chain. Ends the chain. */ +export function tcpPingThroughJumpHost( + jumpClient: Pick, + host: string, + port: number, + timeoutMs = 5000, +): Promise { + return new Promise((resolve) => { + let settled = false; + + const finish = (result: boolean) => { + if (settled) return; + settled = true; + clearTimeout(timeout); + jumpClient.end(); + resolve(result); + }; + + const timeout = setTimeout(() => finish(false), timeoutMs); + + jumpClient.forwardOut("127.0.0.1", 0, host, port, (error, stream) => { + stream?.destroy(); + finish(!error && !!stream); + }); + }); +} diff --git a/src/backend/plugins/ctx-hosts.ts b/src/backend/plugins/ctx-hosts.ts index a56b92bfa..c229672f8 100644 --- a/src/backend/plugins/ctx-hosts.ts +++ b/src/backend/plugins/ctx-hosts.ts @@ -25,6 +25,8 @@ 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"; +import { hostStatusService } from "../hosts/status/host-status-service.js"; +import type { DisposableBag } from "./disposables.js"; type AuditFn = ( action: string, @@ -34,6 +36,8 @@ type AuditFn = ( interface Deps { manifest: PluginManifest; + /** Where registerPort entries go, so deactivate removes them. */ + bag?: DisposableBag; audit: AuditFn; } @@ -98,11 +102,16 @@ function actingUser(): string { return actor; } -export function createPluginHosts({ manifest, audit }: Deps): PluginHosts { +export function createPluginHosts({ manifest, bag, audit }: Deps): PluginHosts { const pluginId = manifest.id; const declared = manifest.capabilities; const requireRead = () => assertCapability(pluginId, "hosts:read", declared); + const requireReadSync = () => { + if (!declared.includes("hosts:read")) { + throw new PluginCapabilityError(pluginId, "hosts:read"); + } + }; const requireWrite = () => assertCapability(pluginId, "hosts:write", declared); @@ -418,5 +427,34 @@ export function createPluginHosts({ manifest, audit }: Deps): PluginHosts { await import("../services/recent-activity.js"); await recordRecentActivity(userId, { type, hostId, hostName }); }, + + // Synchronous and not audited, like trackSession: pollers call these on + // every sample. + status: { + get: (hostId) => { + requireReadSync(); + return hostStatusService.get(hostId); + }, + check: async (hostId) => { + await requireRead(); + return hostStatusService.check(hostId); + }, + reportLogin: (hostId, outcome) => { + requireReadSync(); + hostStatusService.reportLogin(hostId, outcome); + }, + registerPort: (connectionType, resolve) => { + requireReadSync(); + const unregister = hostStatusService.registerPort( + connectionType, + resolve, + ); + const drop = bag?.add(unregister, `status port for ${connectionType}`); + return () => { + drop?.(); + unregister(); + }; + }, + }, }; } diff --git a/src/backend/plugins/ctx-ssh-auth.ts b/src/backend/plugins/ctx-ssh-auth.ts index be733c5e4..a0a1c9d80 100644 --- a/src/backend/plugins/ctx-ssh-auth.ts +++ b/src/backend/plugins/ctx-ssh-auth.ts @@ -90,7 +90,9 @@ export function createPluginSsh({ manifest, bag, audit }: Deps): PluginSsh { const pluginId = manifest.id; const declared = manifest.capabilities; const open = new Set<() => void>(); - const poolKeys = new Set(); + // Pool key -> host id, so dropPooled can find a host's keys after its + // address changed. + const poolKeys = new Map(); bag.add(async () => { for (const dispose of [...open]) dispose(); @@ -98,7 +100,9 @@ export function createPluginSsh({ manifest, bag, audit }: Deps): PluginSsh { if (poolKeys.size > 0) { const { connectionPool } = await import("../hosts/ssh-connection-pool.js"); - for (const key of poolKeys) connectionPool.clearKeyConnections(key); + for (const key of poolKeys.keys()) { + connectionPool.clearKeyConnections(key); + } poolKeys.clear(); } }, "SSH connections"); @@ -183,7 +187,7 @@ export function createPluginSsh({ manifest, bag, audit }: Deps): PluginSsh { actingUser(host), ); const key = poolKey(options.pool, resolved as PluginSshHost); - poolKeys.add(key); + poolKeys.set(key, resolved.id); const { withConnection } = await import("../hosts/ssh-connection-pool.js"); return withConnection( @@ -228,6 +232,19 @@ export function createPluginSsh({ manifest, bag, audit }: Deps): PluginSsh { poolKey, + dropPooled: (pool, hostId) => { + const keys = [...poolKeys.entries()] + .filter(([key, id]) => id === hostId && key.startsWith(`${pool}:`)) + .map(([key]) => key); + if (keys.length === 0) return; + for (const key of keys) poolKeys.delete(key); + void import("../hosts/ssh-connection-pool.js").then( + ({ connectionPool }) => { + for (const key of keys) connectionPool.clearKeyConnections(key); + }, + ); + }, + resolveHost: async (hostId, options) => { await checkSsh(true); const userId = actingUser(hostId); diff --git a/src/backend/plugins/ctx.ts b/src/backend/plugins/ctx.ts index bb3d1b2dd..ace0ac462 100644 --- a/src/backend/plugins/ctx.ts +++ b/src/backend/plugins/ctx.ts @@ -53,6 +53,7 @@ import { } from "../database/db/dialect.js"; import { createPluginAuth, createPluginSsh } from "./ctx-ssh-auth.js"; import { createPluginHosts } from "./ctx-hosts.js"; +import { createPluginSchedule } from "./schedule.js"; import { createPluginCredentials } from "./ctx-credentials.js"; import { isElectronIpcAvailable } from "../utils/electron-ipc-bridge.js"; @@ -425,11 +426,15 @@ export function createPluginContext( refs: () => dbRefs() as never, // Gated but not audited: it follows every write, and the write itself // went through a client() call that already left an audit line. - persist: async () => { + persist: async (options) => { await assertCapability(pluginId, "db:own", manifest.capabilities); if (!needsExplicitPersist(resolveDatabaseDialect())) return; const { DatabaseSaveTrigger } = await import("../utils/database-save-trigger.js"); + if (options?.lazy) { + DatabaseSaveTrigger.triggerSave(`plugin_${pluginId}_write`); + return; + } await DatabaseSaveTrigger.forceSave(`plugin_${pluginId}_write`); }, get dialect() { @@ -715,7 +720,15 @@ export function createPluginContext( }, }, - hosts: createPluginHosts({ manifest, audit: auditCall }), + hosts: createPluginHosts({ manifest, bag: handle.bag, audit: auditCall }), + + schedule: createPluginSchedule(handle.bag, (message, error) => + pluginLogger.error( + message, + error instanceof Error ? error : new Error(String(error)), + logContext, + ), + ), ssh: createPluginSsh({ manifest, bag: handle.bag, audit: auditCall }), auth: createPluginAuth({ manifest, bag: handle.bag, audit: auditCall }), diff --git a/src/backend/plugins/events.ts b/src/backend/plugins/events.ts index 4e98cd4ae..e60524381 100644 --- a/src/backend/plugins/events.ts +++ b/src/backend/plugins/events.ts @@ -1,9 +1,8 @@ /** * The process-wide event bus behind ctx.events. * - * This formalises three ad-hoc publish paths that already existed: + * This formalises the ad-hoc publish paths that already existed: * - hosts/automation-events.ts (generic named internal events) - * - hosts/metrics/automation-bridge.ts (metrics-shaped siblings) * - hosts/host-session-status.ts (the "host.session.status" topic) * * Each of those lazily imported the automations engine and swallowed errors, @@ -28,12 +27,10 @@ export const TOPICS = { hostSessionStatus: "host.session.status", /** A generic named internal event (host_deleted, user_login, ...). */ internalEvent: "internal.event", - /** A metrics snapshot for one host. */ - hostMetrics: "host.metrics", - /** A host went online or offline as seen by the metrics poller. */ + /** A host's status dot changed (hosts/status/host-status-service.ts). */ hostStatus: "host.status", - /** A health check result. */ - hostHealthCheck: "host.health_check", + /** A host's key was accepted again after it changed. */ + hostKeyUpdated: "host.key.updated", /** A host's connection details changed and pollers should re-read them. */ hostUpdated: "host.updated", /** A host was deleted and any poller holding it should drop it. */ diff --git a/src/backend/plugins/schedule.ts b/src/backend/plugins/schedule.ts new file mode 100644 index 000000000..9f9a8d4b2 --- /dev/null +++ b/src/backend/plugins/schedule.ts @@ -0,0 +1,81 @@ +/** + * ctx.schedule: timers a plugin owns. They live in the plugin's disposable + * bag, so deactivate clears every one of them. + */ + +import type { PluginSchedule } from "@termix/plugin-sdk/backend"; +import type { DisposableBag } from "./disposables.js"; + +type Log = (message: string, error: unknown) => void; + +export function createPluginSchedule( + bag: DisposableBag, + logError: Log, +): PluginSchedule { + const run = async (fn: () => void | Promise) => { + try { + await fn(); + } catch (error) { + logError("Scheduled job failed", error); + } + }; + + return { + every: (intervalMs, fn, options) => { + let running = false; + let stopped = false; + let interval: NodeJS.Timeout | undefined; + + const tick = () => { + if (running || stopped) return; + running = true; + void run(fn).finally(() => { + running = false; + }); + }; + + const jitter = Math.max(0, Math.floor(options?.jitterMs ?? 0)); + const start = setTimeout( + () => { + if (stopped) return; + interval = setInterval(tick, Math.max(1, intervalMs)); + if (jitter > 0) tick(); + }, + jitter > 0 ? Math.floor(Math.random() * jitter) : 0, + ); + if (options?.runNow) tick(); + + let drop = () => {}; + const stop = () => { + if (stopped) return; + stopped = true; + clearTimeout(start); + if (interval) clearInterval(interval); + drop(); + }; + drop = bag.add(stop, "scheduled job"); + return stop; + }, + + after: (delayMs, fn) => { + let done = false; + let drop = () => {}; + const timer = setTimeout( + () => { + done = true; + drop(); + void run(fn); + }, + Math.max(0, delayMs), + ); + const cancel = () => { + if (done) return; + done = true; + clearTimeout(timer); + drop(); + }; + drop = bag.add(cancel, "scheduled job"); + return cancel; + }, + }; +} diff --git a/src/backend/plugins/settings.ts b/src/backend/plugins/settings.ts index a7386a391..0856d94ba 100644 --- a/src/backend/plugins/settings.ts +++ b/src/backend/plugins/settings.ts @@ -38,11 +38,7 @@ export type { PluginSettingsScope }; * to it means deciding that every plugin holding settings:read-core may see * that value, so it is a review decision rather than a convenience. */ -export const CORE_SETTINGS_ALLOWLIST: readonly string[] = [ - "app_name", - "global_metrics_interval", - "global_status_check_interval", -]; +export const CORE_SETTINGS_ALLOWLIST: readonly string[] = ["app_name"]; type ChangeListener = (value: unknown) => void; diff --git a/src/backend/starter.ts b/src/backend/starter.ts index d81b035ad..9dc96dfa3 100644 --- a/src/backend/starter.ts +++ b/src/backend/starter.ts @@ -269,6 +269,14 @@ async function provisionLocalDesktopUserIfNeeded(): Promise { await import("./utils/crypto-migration/external-identity-migration.js"); await runExternalIdentityMigration(); + const { runHostStatusConfigMigration } = + await import("./utils/crypto-migration/host-status-config-migration.js"); + await runHostStatusConfigMigration(); + + const { hostStatusService } = + await import("./hosts/status/host-status-service.js"); + hostStatusService.start(); + if (process.env.ELECTRON_EMBEDDED === "true") { await provisionLocalDesktopUserIfNeeded(); } @@ -381,6 +389,10 @@ async function provisionLocalDesktopUserIfNeeded(): Promise { const { runRemoteDesktopSettingsMigration } = await import("./utils/crypto-migration/remote-desktop-settings-migration.js"); await runRemoteDesktopSettingsMigration(); + + const { runHostMetricsSettingsMigration } = + await import("./utils/crypto-migration/host-metrics-settings-migration.js"); + await runHostMetricsSettingsMigration(); } catch (error) { systemLogger.warn("Plugin runtime failed to initialize", { operation: "plugin_init", diff --git a/src/backend/tests/database/repositories/host-credential-repositories.test.ts b/src/backend/tests/database/repositories/host-credential-repositories.test.ts index fd7ec3fbb..c4f99b54b 100644 --- a/src/backend/tests/database/repositories/host-credential-repositories.test.ts +++ b/src/backend/tests/database/repositories/host-credential-repositories.test.ts @@ -483,7 +483,6 @@ describe("HostRepository and CredentialRepository", () => { port: 22, username: "root", authType: "password", - statsConfig: JSON.stringify({ cpu: true }), }); const second = await repo.hosts.create({ userId: "user-1", diff --git a/src/backend/tests/database/repositories/host-health-repository.test.ts b/src/backend/tests/database/repositories/host-health-repository.test.ts deleted file mode 100644 index 99058e343..000000000 --- a/src/backend/tests/database/repositories/host-health-repository.test.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { afterEach, describe, expect, it } from "vitest"; -import { TestSqliteDatabase } from "./test-support.js"; -import { HostHealthRepository } from "../../../database/repositories/host-health-repository.js"; - -describe("HostHealthRepository", () => { - let adapter: TestSqliteDatabase | null = null; - - afterEach(async () => { - if (adapter) { - await adapter.close(); - adapter = null; - } - }); - - async function createRepository( - onWrite?: () => void | Promise, - ): Promise { - adapter = new TestSqliteDatabase(); - const context = await adapter.connect(); - await adapter.exec(` - INSERT INTO users (id, username, password_hash) - VALUES ('user-1', 'alice', 'hash'), ('user-2', 'bob', 'hash'); - INSERT INTO ssh_data (id, user_id, name, ip, port, username, auth_type) - VALUES (1, 'user-1', 'one', '10.0.0.1', 22, 'root', 'password'), (2, 'user-2', 'two', '10.0.0.1', 22, 'root', 'password'); - INSERT INTO host_health_checks ( - user_id, host_id, checks, interval_seconds, created_at, updated_at - ) - VALUES ( - 'user-1', - 1, - '[{"id":"tcp","name":"TCP","type":"tcp","target":"localhost","port":22}]', - 300, - '2026-01-01T00:00:00.000Z', - '2026-01-01T00:00:00.000Z' - ); - `); - - return new HostHealthRepository(context, onWrite); - } - - it("finds and upserts check configuration", async () => { - let writeCount = 0; - const repo = await createRepository(() => { - writeCount += 1; - }); - - const existing = await repo.findChecksByUserAndHost("user-1", 1); - expect(existing?.intervalSeconds).toBe(300); - - const updated = await repo.upsertChecks( - "user-1", - 1, - '[{"id":"http"}]', - 60, - "2026-02-01T00:00:00.000Z", - ); - expect(updated).toMatchObject({ - id: existing?.id, - checks: '[{"id":"http"}]', - intervalSeconds: 60, - updatedAt: "2026-02-01T00:00:00.000Z", - }); - - const created = await repo.upsertChecks( - "user-2", - 2, - '[{"id":"tcp"}]', - 120, - "2026-03-01T00:00:00.000Z", - ); - expect(created).toMatchObject({ - userId: "user-2", - hostId: 2, - checks: '[{"id":"tcp"}]', - intervalSeconds: 120, - createdAt: "2026-03-01T00:00:00.000Z", - updatedAt: "2026-03-01T00:00:00.000Z", - }); - expect(writeCount).toBe(2); - }); - - it("records and prunes history", async () => { - let writeCount = 0; - const repo = await createRepository(() => { - writeCount += 1; - }); - - expect( - await repo.recordHistory( - "user-1", - 1, - [{ checkId: "one", ok: true, latencyMs: 12, detail: "open" }], - 1, - "2026-01-01T00:00:00.000Z", - ), - ).toBe(1); - expect( - await repo.recordHistory( - "user-1", - 1, - [{ checkId: "two", ok: false, latencyMs: null, detail: "closed" }], - 1, - "2026-01-02T00:00:00.000Z", - ), - ).toBe(1); - - const history = await repo.listHistory("user-1", 1, 10); - expect(history).toHaveLength(1); - expect(history[0]).toMatchObject({ - userId: "user-1", - hostId: 1, - checkId: "two", - ok: false, - latencyMs: null, - detail: "closed", - }); - expect(writeCount).toBe(2); - }); - - it("deletes all health checks and history for a user", async () => { - let writeCount = 0; - const repo = await createRepository(() => { - writeCount += 1; - }); - - await repo.upsertChecks("user-2", 2, '[{"id":"tcp"}]', 120); - await repo.recordHistory( - "user-1", - 1, - [{ checkId: "one", ok: true, latencyMs: 12, detail: "open" }], - 10, - ); - await repo.recordHistory( - "user-2", - 2, - [{ checkId: "two", ok: false, latencyMs: null, detail: "closed" }], - 10, - ); - - await expect(repo.deleteByUserId("user-1")).resolves.toEqual({ - checksDeleted: 1, - historyDeleted: 1, - }); - await expect(repo.deleteByUserId("missing")).resolves.toEqual({ - checksDeleted: 0, - historyDeleted: 0, - }); - - expect(await repo.findChecksByUserAndHost("user-1", 1)).toBeNull(); - expect(await repo.listHistory("user-1", 1, 10)).toEqual([]); - expect((await repo.findChecksByUserAndHost("user-2", 2))?.hostId).toBe(2); - expect(await repo.listHistory("user-2", 2, 10)).toHaveLength(1); - expect(writeCount).toBe(4); - }); -}); diff --git a/src/backend/tests/database/repositories/host-repository.test.ts b/src/backend/tests/database/repositories/host-repository.test.ts index 1ff9922b2..828deda7c 100644 --- a/src/backend/tests/database/repositories/host-repository.test.ts +++ b/src/backend/tests/database/repositories/host-repository.test.ts @@ -148,7 +148,6 @@ describe("HostRepository Proxmox sync inserts", () => { tunnelConnections: "[]", jumpHosts: null, quickActions: null, - statsConfig: null, dockerConfig: null, terminalConfig: null, forceKeyboardInteractive: "false", diff --git a/src/backend/tests/database/routes/host-status-routes.test.ts b/src/backend/tests/database/routes/host-status-routes.test.ts new file mode 100644 index 000000000..c7990cc92 --- /dev/null +++ b/src/backend/tests/database/routes/host-status-routes.test.ts @@ -0,0 +1,197 @@ +import http from "node:http"; +import type { AddressInfo } from "node:net"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import express, { type RequestHandler } from "express"; + +const state = vi.hoisted(() => ({ + settings: {} as Record, + locked: false, + accessible: new Set([1, 2]), + statuses: new Map(), +})); + +const service = vi.hoisted(() => ({ + statusesFor: vi.fn(), + get: vi.fn(), + check: vi.fn(), + refresh: vi.fn(), + retimeAll: vi.fn(), +})); + +vi.mock("../../../utils/logger.js", () => ({ + sshLogger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() }, +})); +vi.mock("../../../utils/data-crypto.js", () => ({ + DataCrypto: { getUserDataKey: () => (state.locked ? null : "key") }, +})); +vi.mock("../../../utils/permission-manager.js", () => ({ + PermissionManager: { + getInstance: () => ({ + filterAccessibleHostIds: async (_user: string, ids: number[]) => + new Set(ids.filter((id) => state.accessible.has(id))), + canAccessHost: async (_user: string, id: number) => ({ + hasAccess: state.accessible.has(id), + }), + }), + }, +})); +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; + }, + }), +})); +vi.mock("../../../hosts/status/host-status-service.js", () => ({ + DEFAULT_STATUS_INTERVAL: 60, + GLOBAL_STATUS_INTERVAL_KEY: "global_status_check_interval", + hostStatusService: service, +})); + +const { registerHostStatusRoutes, parseStatusHostIds } = + await import("../../../database/routes/host-status-routes.js"); + +let server: http.Server | null = null; +let base = ""; + +async function start(isAdmin = true) { + const router = express.Router(); + const authenticateJWT: RequestHandler = (req, _res, next) => { + (req as unknown as { userId: string }).userId = "user-1"; + next(); + }; + const requireAdmin: RequestHandler = (_req, res, next) => + isAdmin ? next() : void res.status(403).json({ error: "Admin only" }); + registerHostStatusRoutes(router, { authenticateJWT, requireAdmin }); + const app = express(); + app.use(express.json()); + app.use("/host", router); + server = http.createServer(app); + await new Promise((resolve) => server!.listen(0, resolve)); + base = `http://127.0.0.1:${(server.address() as AddressInfo).port}`; +} + +async function call(method: string, path: string, body?: unknown) { + const response = await fetch(`${base}${path}`, { + method, + headers: body ? { "content-type": "application/json" } : {}, + body: body ? JSON.stringify(body) : undefined, + }); + return { status: response.status, body: await response.json() }; +} + +beforeEach(() => { + state.settings = {}; + state.locked = false; + state.accessible = new Set([1, 2]); + state.statuses = new Map([ + [1, { status: "online", lastChecked: "t" }], + [2, { status: "offline", lastChecked: "t" }], + [3, { status: "reachable", lastChecked: "t" }], + ]); + service.statusesFor.mockImplementation(async () => state.statuses); + service.get.mockImplementation( + (id: number) => state.statuses.get(id) ?? null, + ); + service.check.mockResolvedValue(null); +}); + +afterEach(async () => { + if (server) + await new Promise((resolve) => server!.close(() => resolve())); + server = null; + vi.clearAllMocks(); +}); + +describe("GET /host/status", () => { + it("only returns hosts the user can reach", async () => { + await start(); + const { status, body } = await call("GET", "/host/status"); + expect(status).toBe(200); + expect(Object.keys(body)).toEqual(["1", "2"]); + expect(service.statusesFor).toHaveBeenCalledWith("user-1", null); + }); + + it("narrows to the requested hosts", async () => { + await start(); + const { body } = await call("GET", "/host/status?hostIds=2"); + expect(Object.keys(body)).toEqual(["2"]); + expect(service.statusesFor).toHaveBeenCalledWith("user-1", new Set([2])); + }); + + it("answers 401 while the user's data is locked", async () => { + await start(); + state.locked = true; + expect((await call("GET", "/host/status")).status).toBe(401); + }); +}); + +describe("GET /host/status/:id", () => { + it("returns one host's status", async () => { + await start(); + const { status, body } = await call("GET", "/host/status/1"); + expect(status).toBe(200); + expect(body.status).toBe("online"); + }); + + it("hides a host the user cannot reach", async () => { + await start(); + expect((await call("GET", "/host/status/3")).status).toBe(404); + }); +}); + +describe("status settings", () => { + it("reads the default interval", async () => { + await start(); + expect((await call("GET", "/host/status/settings")).body).toEqual({ + statusCheckInterval: 60, + }); + }); + + it("saves a new interval and re-times the checks", async () => { + await start(); + const { status } = await call("PUT", "/host/status/settings", { + statusCheckInterval: 90, + }); + expect(status).toBe(200); + expect(state.settings.global_status_check_interval).toBe("90"); + expect(service.retimeAll).toHaveBeenCalledOnce(); + }); + + it("rejects an interval out of range", async () => { + await start(); + expect( + (await call("PUT", "/host/status/settings", { statusCheckInterval: 1 })) + .status, + ).toBe(400); + }); + + it("is admin only", async () => { + await start(false); + expect((await call("GET", "/host/status/settings")).status).toBe(403); + expect( + (await call("PUT", "/host/status/settings", { statusCheckInterval: 90 })) + .status, + ).toBe(403); + }); +}); + +describe("POST /host/status/refresh", () => { + it("restarts the user's checks", async () => { + await start(); + expect((await call("POST", "/host/status/refresh")).status).toBe(200); + expect(service.refresh).toHaveBeenCalledWith("user-1"); + }); +}); + +describe("parseStatusHostIds", () => { + it("tells an unrestricted request from an empty host set", () => { + expect(parseStatusHostIds(undefined)).toBeNull(); + expect(parseStatusHostIds("")).toEqual(new Set()); + }); + + it("keeps only valid positive host ids", () => { + expect(parseStatusHostIds("7,2,7,-1,nope,1.5")).toEqual(new Set([7, 2])); + }); +}); diff --git a/src/backend/tests/hosts/host-key-recovery.test.ts b/src/backend/tests/hosts/host-key-recovery.test.ts index 9513112af..61e0b0ea7 100644 --- a/src/backend/tests/hosts/host-key-recovery.test.ts +++ b/src/backend/tests/hosts/host-key-recovery.test.ts @@ -1,8 +1,8 @@ import { EventEmitter } from "node:events"; import type { WebSocket } from "ws"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, describe, expect, it, vi } from "vitest"; import { SSHHostKeyVerifier } from "../../hosts/host-key-verifier.js"; -import { authFailureTracker } from "../../hosts/metrics-shared/state.js"; +import { pluginEvents, TOPICS } from "../../plugins/events.js"; const { updateHostKey } = vi.hoisted(() => ({ updateHostKey: vi.fn() })); vi.mock("../../database/repositories/factory.js", () => ({ @@ -12,10 +12,15 @@ vi.mock("../../utils/logger.js", () => ({ sshLogger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() }, })); const hostId = 991267; +const accepted: unknown[] = []; +const unsubscribe = pluginEvents.on(TOPICS.hostKeyUpdated, (payload) => + accepted.push(payload), +); afterEach(() => { - authFailureTracker.reset(hostId); + accepted.length = 0; vi.resetAllMocks(); }); +afterAll(() => unsubscribe()); async function verifyChangedKey(action: "accept" | "reject") { const socket = new EventEmitter() as EventEmitter & { @@ -52,35 +57,22 @@ async function verifyChangedKey(action: "accept" | "reject") { ); } -describe("metrics recovery after host key verification", () => { - it("allows polling after accepting and saving the replacement key", async () => { - authFailureTracker.recordFailure(hostId, "HOST_KEY", true); - expect(authFailureTracker.shouldSkip(hostId)).toBe(true); +describe("host key recovery", () => { + it("announces an accepted and saved replacement key", async () => { expect(await verifyChangedKey("accept")).toBe(true); expect(updateHostKey).toHaveBeenCalledOnce(); - expect(authFailureTracker.shouldSkip(hostId)).toBe(false); + expect(accepted).toEqual([{ hostId }]); }); - it("keeps polling blocked when the new key is rejected", async () => { - authFailureTracker.recordFailure(hostId, "HOST_KEY", true); + it("announces nothing when the new key is rejected", async () => { expect(await verifyChangedKey("reject")).toBe(false); expect(updateHostKey).not.toHaveBeenCalled(); - expect(authFailureTracker.shouldSkip(hostId)).toBe(true); + expect(accepted).toEqual([]); }); - it("keeps polling blocked if persisting the accepted key fails", async () => { - authFailureTracker.recordFailure(hostId, "HOST_KEY", true); + it("announces nothing if persisting the accepted key fails", async () => { updateHostKey.mockRejectedValueOnce(new Error("database unavailable")); expect(await verifyChangedKey("accept")).toBe(false); - expect(authFailureTracker.shouldSkip(hostId)).toBe(true); + expect(accepted).toEqual([]); }); - - it.each(["AUTH", "TOTP"] as const)( - "preserves unrelated %s failures", - async (reason) => { - authFailureTracker.recordFailure(hostId, reason, true); - expect(await verifyChangedKey("accept")).toBe(true); - expect(authFailureTracker.shouldSkip(hostId)).toBe(true); - }, - ); }); diff --git a/src/backend/tests/hosts/host-resolver.test.ts b/src/backend/tests/hosts/host-resolver.test.ts index c0ca69878..44ca995c1 100644 --- a/src/backend/tests/hosts/host-resolver.test.ts +++ b/src/backend/tests/hosts/host-resolver.test.ts @@ -122,7 +122,6 @@ function baseHost(overrides: Record = {}) { autostartKeyPassword: null, jumpHosts: null, tunnelConnections: null, - statsConfig: null, terminalConfig: null, socks5ProxyChain: null, quickActions: null, diff --git a/src/backend/tests/hosts/status/host-status-service.test.ts b/src/backend/tests/hosts/status/host-status-service.test.ts new file mode 100644 index 000000000..ab65a6e90 --- /dev/null +++ b/src/backend/tests/hosts/status/host-status-service.test.ts @@ -0,0 +1,259 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("../../../database/repositories/factory.js", () => ({ + createCurrentHostResolutionRepository: vi.fn(), + getCurrentSettingValue: vi.fn(() => null), +})); +vi.mock("../../../utils/logger.js", () => ({ + sshLogger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() }, + pluginLogger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() }, +})); + +const { HostStatusService, toStatusTarget } = + await import("../../../hosts/status/host-status-service.js"); +const { hostSessionStatus } = + await import("../../../hosts/host-session-status.js"); +const { pluginEvents, TOPICS } = await import("../../../plugins/events.js"); + +type Target = ReturnType; + +function target(id: number, extra: Partial = {}): Target { + return { + id, + userId: "owner", + ip: `10.0.0.${id}`, + port: 22, + connectionType: "ssh", + jumpHosts: [], + statusCheckEnabled: true, + statusCheckInterval: null, + ...extra, + }; +} + +function setup(targets: Target[], reachable = true) { + const emitted: unknown[] = []; + const ping = vi.fn(async () => reachable); + const pingThroughJumpHosts = vi.fn(async () => reachable); + const loadTargets = vi.fn( + async (filter: { userId?: string; hostIds?: number[] }) => + targets.filter( + (t) => + (!filter.userId || t.userId === filter.userId) && + (!filter.hostIds || filter.hostIds.includes(t.id)), + ), + ); + const service = new HostStatusService({ + loadTargets, + ping, + pingThroughJumpHosts, + globalInterval: () => 60, + emit: (payload) => emitted.push(payload), + }); + return { service, emitted, ping, pingThroughJumpHosts, loadTargets }; +} + +const flush = () => new Promise((resolve) => setTimeout(resolve, 0)); + +let active: InstanceType | null = null; + +beforeEach(() => { + vi.useFakeTimers({ toFake: ["setInterval", "clearInterval"] }); +}); + +afterEach(() => { + active?.stop(); + active = null; + pluginEvents.clear(); + vi.useRealTimers(); +}); + +describe("HostStatusService", () => { + it("starts a user's own hosts and reports them reachable", async () => { + const { service, emitted, ping } = setup([target(1), target(2)]); + active = service; + + await service.statusesFor("owner", null); + await flush(); + + expect(ping).toHaveBeenCalledWith("10.0.0.1", 22); + expect(service.get(1)?.status).toBe("reachable"); + expect(emitted).toContainEqual({ + hostId: 1, + ownerUserId: "owner", + status: "reachable", + previous: null, + online: true, + }); + }); + + it("reports offline for a host that does not answer", async () => { + const { service } = setup([target(1)], false); + active = service; + await service.statusesFor("owner", null); + await flush(); + expect(service.get(1)?.status).toBe("offline"); + }); + + it("never checks a host with status checks off", async () => { + const { service, ping } = setup([target(1, { statusCheckEnabled: false })]); + active = service; + await service.statusesFor("owner", null); + await flush(); + expect(ping).not.toHaveBeenCalled(); + expect(service.get(1)).toBeNull(); + }); + + it("goes through the jump hosts when a host has them", async () => { + const { service, ping, pingThroughJumpHosts } = setup([ + target(1, { jumpHosts: [{ hostId: 9 }] }), + ]); + active = service; + await service.statusesFor("owner", null); + await flush(); + expect(ping).not.toHaveBeenCalled(); + expect(pingThroughJumpHosts).toHaveBeenCalledOnce(); + }); + + it("asks a registered plugin for a protocol's port", async () => { + const { service, ping } = setup([target(1, { connectionType: "rdp" })]); + active = service; + service.registerPort("rdp", async () => 3390); + await service.statusesFor("owner", null); + await flush(); + expect(ping).toHaveBeenCalledWith("10.0.0.1", 3390); + }); + + it("falls back to the host's port when no plugin knows the protocol", async () => { + const { service, ping } = setup([ + target(1, { connectionType: "rdp", port: 3389 }), + ]); + active = service; + await service.statusesFor("owner", null); + await flush(); + expect(ping).toHaveBeenCalledWith("10.0.0.1", 3389); + }); + + it("turns reachable into online when a plugin reports a login", async () => { + const { service, emitted } = setup([target(1)]); + active = service; + await service.statusesFor("owner", null); + await flush(); + + service.reportLogin(1, { ok: true }); + await flush(); + expect(service.get(1)?.status).toBe("online"); + expect(emitted.at(-1)).toMatchObject({ + status: "online", + previous: "reachable", + }); + + service.reportLogin(1, { ok: false, hostKeyChanged: true }); + expect(service.get(1)).toMatchObject({ + status: "reachable", + reason: "host_key_changed", + }); + }); + + it("clears the host key reason once the new key is accepted", async () => { + const { service } = setup([target(1)]); + active = service; + service.start(); + await service.statusesFor("owner", null); + await flush(); + service.reportLogin(1, { ok: false, hostKeyChanged: true }); + + pluginEvents.emit(TOPICS.hostKeyUpdated, { hostId: 1 }); + expect(service.get(1)?.reason).toBeUndefined(); + }); + + it("shows a host with an open terminal session as online", async () => { + const { service } = setup([target(1)]); + active = service; + service.start(); + await service.statusesFor("owner", null); + await flush(); + + const release = hostSessionStatus.register(1); + expect(service.get(1)?.status).toBe("online"); + release(); + expect(service.get(1)?.status).toBe("reachable"); + }); + + it("only checks the requested hosts on the desktop app", async () => { + const { service, ping } = setup([target(1), target(2)]); + active = service; + await service.statusesFor("owner", new Set([2])); + await flush(); + expect(ping).toHaveBeenCalledTimes(1); + expect(ping).toHaveBeenCalledWith("10.0.0.2", 22); + }); + + it("checks again on its interval", async () => { + const { service, ping } = setup([target(1, { statusCheckInterval: 10 })]); + active = service; + await service.statusesFor("owner", null); + await flush(); + expect(ping).toHaveBeenCalledTimes(1); + + vi.advanceTimersByTime(15_000); + await flush(); + expect(ping).toHaveBeenCalledTimes(2); + }); + + it("checks a host on demand unless the last result is fresh", async () => { + const { service, ping } = setup([target(1)]); + active = service; + expect(await service.check(1)).toMatchObject({ status: "reachable" }); + expect(await service.check(1)).toMatchObject({ status: "reachable" }); + expect(ping).toHaveBeenCalledTimes(1); + }); + + it("forgets a deleted host", async () => { + const { service } = setup([target(1)]); + active = service; + service.start(); + await service.statusesFor("owner", null); + await flush(); + + pluginEvents.emit(TOPICS.hostDeleted, { hostId: 1 }); + expect(service.get(1)).toBeNull(); + }); + + it("never logs in to check a host", async () => { + // The only transports are the two ping functions: nothing here can + // authenticate, so a RADIUS or Duo backed host gets no 2FA push. + const { service, ping, pingThroughJumpHosts } = setup([target(1)]); + active = service; + await service.statusesFor("owner", null); + await flush(); + expect(ping.mock.calls[0]).toEqual(["10.0.0.1", 22]); + expect(pingThroughJumpHosts).not.toHaveBeenCalled(); + }); +}); + +describe("toStatusTarget", () => { + it("parses the jump hosts and defaults the connection type", () => { + expect( + toStatusTarget({ + id: 1, + userId: "u", + ip: "h", + port: 22, + connectionType: "", + jumpHosts: '[{"hostId":3},{"bad":1}]', + statusCheckEnabled: false, + statusCheckInterval: 30, + }), + ).toEqual({ + id: 1, + userId: "u", + ip: "h", + port: 22, + connectionType: "ssh", + jumpHosts: [{ hostId: 3 }], + statusCheckEnabled: false, + statusCheckInterval: 30, + }); + }); +}); diff --git a/plugins/host-metrics/tests/backend/metrics-shared/host-status.test.ts b/src/backend/tests/hosts/status/host-status.test.ts similarity index 94% rename from plugins/host-metrics/tests/backend/metrics-shared/host-status.test.ts rename to src/backend/tests/hosts/status/host-status.test.ts index 20658fcbc..b12206346 100644 --- a/plugins/host-metrics/tests/backend/metrics-shared/host-status.test.ts +++ b/src/backend/tests/hosts/status/host-status.test.ts @@ -3,7 +3,7 @@ import { isHostKeyVerificationError, statusAfterAuthentication, statusAfterReachabilityCheck, -} from "../../../../../src/backend/hosts/metrics-shared/host-status.js"; +} from "../../../hosts/status/host-status.js"; describe("isHostKeyVerificationError", () => { it.each([ diff --git a/src/backend/tests/hosts/status/limiter.test.ts b/src/backend/tests/hosts/status/limiter.test.ts new file mode 100644 index 000000000..313226c8c --- /dev/null +++ b/src/backend/tests/hosts/status/limiter.test.ts @@ -0,0 +1,162 @@ +import { describe, expect, it } from "vitest"; +import { ConcurrentLimiter } from "../../../hosts/status/limiter.js"; + +describe("ConcurrentLimiter", () => { + it("never exceeds max concurrent runners", async () => { + const limiter = new ConcurrentLimiter(2); + let peak = 0; + let current = 0; + + const job = async () => { + current += 1; + peak = Math.max(peak, current); + await new Promise((r) => setTimeout(r, 30)); + current -= 1; + }; + + await Promise.all([ + limiter.run(job), + limiter.run(job), + limiter.run(job), + limiter.run(job), + ]); + + expect(peak).toBeLessThanOrEqual(2); + expect(limiter.activeCount).toBe(0); + expect(limiter.pendingCount).toBe(0); + }); + + it("runs waiters in FIFO order after a slot frees", async () => { + const limiter = new ConcurrentLimiter(1); + const order: number[] = []; + + const first = limiter.run(async () => { + order.push(1); + await new Promise((r) => setTimeout(r, 20)); + }); + const second = limiter.run(async () => { + order.push(2); + }); + const third = limiter.run(async () => { + order.push(3); + }); + + await Promise.all([first, second, third]); + expect(order).toEqual([1, 2, 3]); + }); + + it("rejects invalid maxConcurrent", () => { + expect(() => new ConcurrentLimiter(0)).toThrow(/maxConcurrent/); + }); + + describe("setLimit", () => { + it("releases queued waiters as soon as the ceiling is raised", async () => { + const limiter = new ConcurrentLimiter(1); + let running = 0; + let peak = 0; + const release: Array<() => void> = []; + + const job = () => + limiter.run(async () => { + running += 1; + peak = Math.max(peak, running); + await new Promise((r) => release.push(r)); + running -= 1; + }); + + const jobs = [job(), job(), job(), job()]; + await new Promise((r) => setTimeout(r, 10)); + expect(peak).toBe(1); + expect(limiter.pendingCount).toBe(3); + + // Widening must drain the backlog without waiting for the running job. + limiter.setLimit(4); + await new Promise((r) => setTimeout(r, 10)); + expect(peak).toBe(4); + expect(limiter.pendingCount).toBe(0); + + release.forEach((fn) => fn()); + await Promise.all(jobs); + expect(limiter.activeCount).toBe(0); + }); + + it("does not over-release beyond the new ceiling", async () => { + const limiter = new ConcurrentLimiter(1); + let running = 0; + let peak = 0; + const release: Array<() => void> = []; + // Later waves of woken jobs enqueue their own resolvers, so draining has + // to keep going until nothing is left rather than flushing a snapshot. + const drain = async () => { + while (release.length > 0) { + release.splice(0).forEach((fn) => fn()); + await new Promise((r) => setTimeout(r, 5)); + } + }; + + const job = () => + limiter.run(async () => { + running += 1; + peak = Math.max(peak, running); + await new Promise((r) => release.push(r)); + running -= 1; + }); + + const jobs = [job(), job(), job(), job(), job()]; + await new Promise((r) => setTimeout(r, 10)); + + limiter.setLimit(3); + await new Promise((r) => setTimeout(r, 10)); + expect(peak).toBe(3); + expect(limiter.pendingCount).toBe(2); + + await drain(); + await Promise.all(jobs); + expect(limiter.activeCount).toBe(0); + expect(limiter.pendingCount).toBe(0); + }); + + it("lets running work finish when the ceiling shrinks", async () => { + const limiter = new ConcurrentLimiter(4); + let running = 0; + let peak = 0; + const release: Array<() => void> = []; + const drain = async () => { + while (release.length > 0) { + release.splice(0).forEach((fn) => fn()); + await new Promise((r) => setTimeout(r, 5)); + } + }; + + const job = () => + limiter.run(async () => { + running += 1; + peak = Math.max(peak, running); + await new Promise((r) => release.push(r)); + running -= 1; + }); + + const jobs = [job(), job(), job(), job(), job(), job()]; + await new Promise((r) => setTimeout(r, 10)); + expect(peak).toBe(4); + + // Shrinking never kills in-flight work; it applies to later releases. + limiter.setLimit(2); + expect(limiter.activeCount).toBe(4); + + await drain(); + await Promise.all(jobs); + expect(limiter.activeCount).toBe(0); + expect(limiter.pendingCount).toBe(0); + // The two queued jobs ran only after the shrink, so they never pushed + // occupancy back up to the old width. + expect(peak).toBe(4); + }); + + it("rejects an invalid new limit", () => { + const limiter = new ConcurrentLimiter(2); + expect(() => limiter.setLimit(0)).toThrow(/maxConcurrent/); + expect(limiter.limit).toBe(2); + }); + }); +}); diff --git a/src/backend/tests/hosts/status/tcp-ping.test.ts b/src/backend/tests/hosts/status/tcp-ping.test.ts new file mode 100644 index 000000000..c9b69f56b --- /dev/null +++ b/src/backend/tests/hosts/status/tcp-ping.test.ts @@ -0,0 +1,57 @@ +import type { Client } from "ssh2"; +import { describe, expect, it, vi } from "vitest"; +import { tcpPingThroughJumpHost } from "../../../hosts/status/tcp-ping.js"; + +describe("tcpPingThroughJumpHost", () => { + it("reports the final destination online when forwarding succeeds", async () => { + const stream = { destroy: vi.fn() }; + const jumpClient = { + end: vi.fn(), + forwardOut: vi.fn((_src, _srcPort, host, port, callback) => { + expect(host).toBe("private.example"); + expect(port).toBe(22); + callback(undefined, stream); + }), + } as unknown as Pick; + + await expect( + tcpPingThroughJumpHost(jumpClient, "private.example", 22), + ).resolves.toBe(true); + expect(stream.destroy).toHaveBeenCalledOnce(); + expect(jumpClient.end).toHaveBeenCalledOnce(); + }); + + it("reports the final destination offline when forwarding fails", async () => { + const jumpClient = { + end: vi.fn(), + forwardOut: vi.fn((_src, _srcPort, _host, _port, callback) => { + callback(new Error("Connection refused")); + }), + } as unknown as Pick; + + await expect( + tcpPingThroughJumpHost(jumpClient, "private.example", 22), + ).resolves.toBe(false); + expect(jumpClient.end).toHaveBeenCalledOnce(); + }); + + it("reports the final destination offline when forwarding times out", async () => { + vi.useFakeTimers(); + const jumpClient = { + end: vi.fn(), + forwardOut: vi.fn(), + } as unknown as Pick; + + const result = tcpPingThroughJumpHost( + jumpClient, + "private.example", + 22, + 5000, + ); + await vi.advanceTimersByTimeAsync(5000); + + await expect(result).resolves.toBe(false); + expect(jumpClient.end).toHaveBeenCalledOnce(); + vi.useRealTimers(); + }); +}); diff --git a/src/backend/tests/plugins/ctx-hosts.test.ts b/src/backend/tests/plugins/ctx-hosts.test.ts index a179b156e..ccf31b545 100644 --- a/src/backend/tests/plugins/ctx-hosts.test.ts +++ b/src/backend/tests/plugins/ctx-hosts.test.ts @@ -22,6 +22,9 @@ const h = vi.hoisted(() => ({ sessions: [] as number[], released: [] as number[], importRows: [] as Array<{ hostId: number; row: Record }>, + statusReports: [] as Array>, + statusPorts: [] as string[], + statusPortsRemoved: [] as string[], roles: [] as Array<{ id: number; name: string; @@ -111,6 +114,22 @@ vi.mock("../../hosts/host-session-status.js", () => ({ }, }, })); +vi.mock("../../hosts/status/host-status-service.js", () => ({ + hostStatusService: { + get: (hostId: number) => + hostId === 4 ? { status: "reachable", lastChecked: "t" } : null, + check: async (hostId: number) => ({ + status: "offline", + lastChecked: String(hostId), + }), + reportLogin: (hostId: number, outcome: Record) => + h.statusReports.push({ hostId, ...outcome }), + registerPort: (connectionType: string) => { + h.statusPorts.push(connectionType); + return () => h.statusPortsRemoved.push(connectionType); + }, + }, +})); vi.mock("../../database/routes/host-plugin-settings.js", () => ({ applyPluginHostImportSettings: async ( hostId: number, @@ -419,3 +438,69 @@ describe("ctx.hosts session tracking and activity", () => { ]); }); }); + +describe("ctx.hosts.status", () => { + it("reads and checks core's status with hosts:read", async () => { + h.granted = new Set(["hosts:read"]); + const hosts = createPluginHosts({ + manifest: manifest(["hosts:read"]), + audit: vi.fn(async () => {}), + }); + expect(hosts.status.get(4)).toEqual({ + status: "reachable", + lastChecked: "t", + }); + expect(hosts.status.get(5)).toBeNull(); + await expect(hosts.status.check(5)).resolves.toEqual({ + status: "offline", + lastChecked: "5", + }); + }); + + it("refuses every status member without hosts:read", async () => { + const hosts = createPluginHosts({ + manifest: manifest([]), + audit: vi.fn(async () => {}), + }); + expect(() => hosts.status.get(4)).toThrow(PluginCapabilityError); + expect(() => hosts.status.reportLogin(4, { ok: true })).toThrow( + PluginCapabilityError, + ); + expect(() => hosts.status.registerPort("rdp", () => 3389)).toThrow( + PluginCapabilityError, + ); + await expect(hosts.status.check(4)).rejects.toBeInstanceOf( + PluginCapabilityError, + ); + }); + + it("passes login reports on to core", () => { + h.statusReports = []; + h.granted = new Set(["hosts:read"]); + const hosts = createPluginHosts({ + manifest: manifest(["hosts:read"]), + audit: vi.fn(async () => {}), + }); + hosts.status.reportLogin(4, { ok: false, hostKeyChanged: true }); + expect(h.statusReports).toEqual([ + { hostId: 4, ok: false, hostKeyChanged: true }, + ]); + }); + + it("removes a registered port when the plugin is disposed", async () => { + h.statusPorts = []; + h.statusPortsRemoved = []; + h.granted = new Set(["hosts:read"]); + const { DisposableBag } = await import("../../plugins/disposables.js"); + const bag = new DisposableBag("fixture"); + const hosts = createPluginHosts({ + manifest: manifest(["hosts:read"]), + bag, + audit: vi.fn(async () => {}), + }); + hosts.status.registerPort("rdp", () => 3389); + expect(h.statusPorts).toEqual(["rdp"]); + await bag.disposeAll(); + expect(h.statusPortsRemoved).toEqual(["rdp"]); + }); +}); diff --git a/src/backend/tests/plugins/ctx-ssh-auth.test.ts b/src/backend/tests/plugins/ctx-ssh-auth.test.ts index fd16d0024..5fcaeae1f 100644 --- a/src/backend/tests/plugins/ctx-ssh-auth.test.ts +++ b/src/backend/tests/plugins/ctx-ssh-auth.test.ts @@ -6,6 +6,9 @@ const h = vi.hoisted(() => ({ connects: [] as Array<{ target: unknown; options: Record }>, factors: [] as Array<{ userId: string; pluginId: string; factorId: string }>, actor: undefined as string | undefined, + cleared: [] as string[], + clearedAll: 0, + pooled: [] as string[], })); vi.mock("../../utils/logger.js", () => { @@ -27,7 +30,33 @@ vi.mock("../../plugins/permissions.js", async () => { }; }); vi.mock("../../plugins/actor.js", () => ({ getActor: () => h.actor })); +vi.mock("../../hosts/ssh-connection-pool.js", () => ({ + withConnection: async ( + key: string, + factory: () => Promise, + fn: (client: unknown) => Promise, + ) => { + h.pooled.push(key); + return fn(await factory()); + }, + connectionPool: { + clearKeyConnections: (key: string) => h.cleared.push(key), + clearAllConnections: () => { + h.clearedAll += 1; + }, + }, +})); vi.mock("../../hosts/connect/connect-host.js", () => ({ + resolveConnectHost: async (target: unknown) => + typeof target === "number" + ? { + id: target, + userId: "user-1", + ip: `10.0.0.${target}`, + port: 22, + username: "root", + } + : target, connectHost: async (target: unknown, options: Record) => { h.connects.push({ target, options }); const client = Object.assign(new EventEmitter(), { end: vi.fn() }); @@ -71,6 +100,9 @@ beforeEach(() => { h.connects = []; h.factors = []; h.actor = "user-1"; + h.cleared = []; + h.clearedAll = 0; + h.pooled = []; }); describe("ctx.ssh", () => { @@ -170,6 +202,62 @@ describe("ctx.ssh", () => { }); }); +describe("ctx.ssh pooled connections", () => { + const both = ["ssh:connect", "credentials:use"]; + + function plugin(id: string) { + const bag = new DisposableBag(id); + const ssh = createPluginSsh({ + manifest: { ...(manifest(both) as object), id } as never, + bag, + audit: vi.fn(async () => {}), + }); + return { bag, ssh }; + } + + it("disabling one plugin leaves ctx.ssh and the pool working for others", async () => { + h.granted = new Set(both); + const metrics = plugin("host-metrics"); + const other = plugin("fleets"); + + await metrics.ssh.withConnection(7, { pool: "stats" }, async () => "a"); + await other.ssh.withConnection(8, { pool: "fleet" }, async () => "b"); + await metrics.bag.disposeAll(); + + expect(h.clearedAll).toBe(0); + expect(h.cleared).toEqual(["stats:user-1:10.0.0.7:22:root"]); + await expect( + other.ssh.withConnection(8, { pool: "fleet" }, async () => "still"), + ).resolves.toBe("still"); + }); + + it("dropPooled clears only this plugin's keys for that host", async () => { + h.granted = new Set(both); + const metrics = plugin("host-metrics"); + await metrics.ssh.withConnection(7, { pool: "stats" }, async () => 1); + await metrics.ssh.withConnection(8, { pool: "stats" }, async () => 1); + + metrics.ssh.dropPooled("stats", 7); + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(h.cleared).toEqual(["stats:user-1:10.0.0.7:22:root"]); + + // Dropped keys are forgotten, so deactivate only clears what is left. + h.cleared = []; + await metrics.bag.disposeAll(); + expect(h.cleared).toEqual(["stats:user-1:10.0.0.8:22:root"]); + }); + + it("dropPooled ignores another pool and an unknown host", async () => { + h.granted = new Set(both); + const metrics = plugin("host-metrics"); + await metrics.ssh.withConnection(7, { pool: "stats" }, async () => 1); + metrics.ssh.dropPooled("other", 7); + metrics.ssh.dropPooled("stats", 99); + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(h.cleared).toEqual([]); + }); +}); + describe("ctx.auth", () => { it("needs auth:provide and a contributes.auth entry to register anything", () => { const auth = createPluginAuth({ diff --git a/src/backend/tests/plugins/events.test.ts b/src/backend/tests/plugins/events.test.ts index f86b79503..4697f7969 100644 --- a/src/backend/tests/plugins/events.test.ts +++ b/src/backend/tests/plugins/events.test.ts @@ -96,9 +96,8 @@ describe("plugin event bus", () => { it("exposes the internal topic names the server publishes", () => { expect(TOPICS.hostSessionStatus).toBe("host.session.status"); expect(TOPICS.internalEvent).toBe("internal.event"); - expect(TOPICS.hostMetrics).toBe("host.metrics"); expect(TOPICS.hostStatus).toBe("host.status"); - expect(TOPICS.hostHealthCheck).toBe("host.health_check"); + expect(TOPICS.hostKeyUpdated).toBe("host.key.updated"); }); }); @@ -135,30 +134,6 @@ describe("hosts publishers route through the bus", () => { expect(listener).not.toHaveBeenCalled(); }); - it("notifyAutomationStatus and notifyAutomationMetrics emit their topics", async () => { - const { notifyAutomationStatus, notifyAutomationMetrics } = - await import("../../../../plugins/host-metrics/src/backend/automation-bridge.js"); - - const status = vi.fn(); - const metrics = vi.fn(); - pluginEvents.on(TOPICS.hostStatus, status); - pluginEvents.on(TOPICS.hostMetrics, metrics); - - notifyAutomationStatus(42, "user-1", true); - notifyAutomationMetrics(42, "user-1", { cpu: 10 } as never); - - expect(status).toHaveBeenCalledWith({ - hostId: 42, - ownerUserId: "user-1", - online: true, - }); - expect(metrics).toHaveBeenCalledWith({ - hostId: 42, - ownerUserId: "user-1", - metrics: { cpu: 10 }, - }); - }); - it("hostSessionStatus publishes host.session.status on the refcount edges", async () => { const { HostSessionStatus } = await import("../../hosts/host-session-status.js"); diff --git a/src/backend/tests/plugins/schedule.test.ts b/src/backend/tests/plugins/schedule.test.ts new file mode 100644 index 000000000..60387df05 --- /dev/null +++ b/src/backend/tests/plugins/schedule.test.ts @@ -0,0 +1,100 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("../../utils/logger.js", () => { + const log = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }; + return { pluginLogger: log }; +}); + +const { createPluginSchedule } = await import("../../plugins/schedule.js"); +const { DisposableBag } = await import("../../plugins/disposables.js"); + +beforeEach(() => { + vi.useFakeTimers(); +}); + +afterEach(() => { + vi.useRealTimers(); +}); + +describe("ctx.schedule", () => { + it("repeats a job and stops it", async () => { + const bag = new DisposableBag("fixture"); + const schedule = createPluginSchedule(bag, vi.fn()); + const job = vi.fn(); + + const stop = schedule.every(1000, job); + await vi.advanceTimersByTimeAsync(3000); + expect(job).toHaveBeenCalledTimes(3); + + stop(); + await vi.advanceTimersByTimeAsync(3000); + expect(job).toHaveBeenCalledTimes(3); + expect(bag.size).toBe(0); + }); + + it("runs straight away when asked", async () => { + const schedule = createPluginSchedule( + new DisposableBag("fixture"), + vi.fn(), + ); + const job = vi.fn(); + schedule.every(1000, job, { runNow: true }); + expect(job).toHaveBeenCalledTimes(1); + }); + + it("never overlaps a slow run with the next tick", async () => { + const schedule = createPluginSchedule( + new DisposableBag("fixture"), + vi.fn(), + ); + let running = 0; + let peak = 0; + schedule.every(100, async () => { + running++; + peak = Math.max(peak, running); + await new Promise((resolve) => setTimeout(resolve, 350)); + running--; + }); + await vi.advanceTimersByTimeAsync(1000); + expect(peak).toBe(1); + }); + + it("logs a failing job instead of throwing", async () => { + const log = vi.fn(); + const schedule = createPluginSchedule(new DisposableBag("fixture"), log); + schedule.every(100, () => { + throw new Error("boom"); + }); + await vi.advanceTimersByTimeAsync(100); + expect(log).toHaveBeenCalledWith("Scheduled job failed", expect.any(Error)); + }); + + it("clears every timer when the plugin is disposed", async () => { + const bag = new DisposableBag("fixture"); + const schedule = createPluginSchedule(bag, vi.fn()); + const repeating = vi.fn(); + const once = vi.fn(); + schedule.every(100, repeating, { jitterMs: 50 }); + schedule.after(500, once); + + await bag.disposeAll(); + await vi.advanceTimersByTimeAsync(2000); + expect(repeating).not.toHaveBeenCalled(); + expect(once).not.toHaveBeenCalled(); + }); + + it("runs a one-off job once and can cancel it", async () => { + const bag = new DisposableBag("fixture"); + const schedule = createPluginSchedule(bag, vi.fn()); + const once = vi.fn(); + schedule.after(100, once); + const cancelled = vi.fn(); + const cancel = schedule.after(100, cancelled); + cancel(); + + await vi.advanceTimersByTimeAsync(500); + expect(once).toHaveBeenCalledOnce(); + expect(cancelled).not.toHaveBeenCalled(); + expect(bag.size).toBe(0); + }); +}); diff --git a/src/backend/tests/utils/host-metrics-settings-migration.test.ts b/src/backend/tests/utils/host-metrics-settings-migration.test.ts new file mode 100644 index 000000000..7677cc45d --- /dev/null +++ b/src/backend/tests/utils/host-metrics-settings-migration.test.ts @@ -0,0 +1,200 @@ +/** + * Host Metrics' settings leaving core: the two admin keys, the new-host + * default inside host_defaults, and the metrics half of every host's + * stats_config. Nothing may be lost, and a second run must change nothing. + */ + +import Database from "better-sqlite3"; +import { drizzle } from "drizzle-orm/better-sqlite3"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +interface Row { + scope: string; + scopeId: string | null; + key: string; + value: string | null; +} + +const state = vi.hoisted(() => ({ + coreSettings: new Map(), + pluginRows: [] as Array<{ + scope: string; + scopeId: string | null; + key: string; + value: string | null; + }>, + pluginInstalled: true, + db: null as unknown, +})); + +const find = (scope: string, scopeId: string | null, key: string) => + state.pluginRows.find( + (row) => row.scope === scope && row.scopeId === scopeId && row.key === key, + ); + +vi.mock("../../database/db/index.js", () => ({ getDb: () => state.db })); +vi.mock("../../database/repositories/factory.js", () => ({ + createCurrentPluginRepository: () => ({ + findById: async (id: string) => + state.pluginInstalled && id === "host-metrics" ? { id } : null, + }), + createCurrentPluginSettingsRepository: () => ({ + get: async ( + _pluginId: string, + scope: string, + scopeId: string | null, + key: string, + ) => find(scope, scopeId, key) ?? null, + getAll: async (_pluginId: string, scope: string, scopeId: string | null) => + state.pluginRows.filter( + (row) => row.scope === scope && row.scopeId === scopeId, + ), + set: async ( + _pluginId: string, + scope: string, + scopeId: string | null, + key: string, + value: string | null, + ) => { + const existing = find(scope, scopeId, key); + if (existing) existing.value = value; + else state.pluginRows.push({ scope, scopeId, key, value }); + }, + }), + createCurrentSettingsRepository: () => ({ + get: async (key: string) => state.coreSettings.get(key) ?? null, + }), +})); +vi.mock("../../utils/logger.js", () => ({ + databaseLogger: { info: vi.fn(), warn: vi.fn() }, +})); + +const { runHostMetricsSettingsMigration, hostSettingsFromStatsConfig } = + await import("../../utils/crypto-migration/host-metrics-settings-migration.js"); + +let sqlite: Database.Database; + +const value = (scope: string, scopeId: string | null, key: string) => { + const row = find(scope, scopeId, key) as Row | undefined; + return row?.value == null ? undefined : JSON.parse(row.value); +}; + +beforeEach(() => { + state.coreSettings.clear(); + state.pluginRows = []; + state.pluginInstalled = true; + sqlite = new Database(":memory:"); + sqlite.exec( + "CREATE TABLE ssh_data (id INTEGER PRIMARY KEY, stats_config TEXT)", + ); + state.db = drizzle(sqlite); +}); + +describe("hostSettingsFromStatsConfig", () => { + it("keeps only what differs from the defaults", () => { + expect( + hostSettingsFromStatsConfig( + JSON.stringify({ + statusCheckEnabled: false, + metricsEnabled: false, + metricsInterval: 45, + useGlobalMetricsInterval: false, + enabledWidgets: ["cpu", "disk"], + excludedMounts: ["/snap"], + monitoredMounts: [{ path: "/data", label: "Data" }, { bad: 1 }], + }), + ), + ).toEqual({ + metricsEnabled: false, + metricsInterval: 45, + enabledWidgets: ["cpu", "disk"], + excludedMounts: ["/snap"], + monitoredMounts: [{ path: "/data", label: "Data" }], + }); + }); + + it("ignores the host's interval while it followed the global one", () => { + expect( + hostSettingsFromStatsConfig(JSON.stringify({ metricsInterval: 45 })), + ).toEqual({}); + }); + + it("reads a value that was stringified twice", () => { + expect( + hostSettingsFromStatsConfig( + JSON.stringify(JSON.stringify({ metricsEnabled: false })), + ), + ).toEqual({ metricsEnabled: false }); + }); +}); + +describe("runHostMetricsSettingsMigration", () => { + it("moves the admin keys, the new-host default and every host", async () => { + state.coreSettings.set("global_metrics_interval", "20"); + state.coreSettings.set("metrics_history_retention_days", "30"); + state.coreSettings.set( + "host_defaults", + JSON.stringify({ metricsEnabled: false, statusCheckEnabled: true }), + ); + const insert = sqlite.prepare( + "INSERT INTO ssh_data (id, stats_config) VALUES (?, ?)", + ); + insert.run(1, JSON.stringify({ metricsEnabled: false })); + insert.run(2, JSON.stringify({ enabledWidgets: ["cpu"] })); + insert.run(3, JSON.stringify({ statusCheckEnabled: false })); + insert.run(4, null); + + const result = await runHostMetricsSettingsMigration(); + + expect(result.moved.sort()).toEqual([ + "global_metrics_interval", + "host_defaults.metricsEnabled", + "metrics_history_retention_days", + ]); + expect(value("admin", null, "metricsInterval")).toBe(20); + expect(value("admin", null, "historyRetentionDays")).toBe(30); + expect(value("admin", null, "enabledForNewHosts")).toBe(false); + expect(value("host", "1", "metricsEnabled")).toBe(false); + expect(value("host", "2", "enabledWidgets")).toEqual(["cpu"]); + // Only the status half, which is core's: nothing for the plugin. + expect(state.pluginRows.filter((row) => row.scopeId === "3")).toEqual([]); + expect(result.hostsMoved).toBe(2); + }); + + it("changes nothing the second time", async () => { + state.coreSettings.set("global_metrics_interval", "20"); + sqlite + .prepare("INSERT INTO ssh_data (id, stats_config) VALUES (?, ?)") + .run(1, JSON.stringify({ metricsEnabled: false })); + await runHostMetricsSettingsMigration(); + + // Changed through the plugin after the upgrade. + const interval = find("admin", null, "metricsInterval")!; + interval.value = "90"; + const enabled = find("host", "1", "metricsEnabled")!; + enabled.value = "true"; + + const second = await runHostMetricsSettingsMigration(); + expect(second).toEqual({ moved: [], hostsMoved: 0, hostsSkipped: 1 }); + expect(value("admin", null, "metricsInterval")).toBe(90); + expect(value("host", "1", "metricsEnabled")).toBe(true); + }); + + it("waits until the plugin row exists", async () => { + state.pluginInstalled = false; + state.coreSettings.set("global_metrics_interval", "20"); + expect(await runHostMetricsSettingsMigration()).toEqual({ + moved: [], + hostsMoved: 0, + hostsSkipped: 0, + }); + expect(state.pluginRows).toEqual([]); + }); + + it("skips values out of range", async () => { + state.coreSettings.set("global_metrics_interval", "1"); + state.coreSettings.set("metrics_history_retention_days", "900"); + await runHostMetricsSettingsMigration(); + expect(state.pluginRows).toEqual([]); + }); +}); diff --git a/src/backend/tests/utils/host-status-config-migration.test.ts b/src/backend/tests/utils/host-status-config-migration.test.ts new file mode 100644 index 000000000..a1c81038a --- /dev/null +++ b/src/backend/tests/utils/host-status-config-migration.test.ts @@ -0,0 +1,143 @@ +/** + * The status half of ssh_data.stats_config moving into the core + * status_check_enabled and status_check_interval columns. Run against a real + * SQLite database through the same raw SQL helpers the boot path uses. + */ + +import Database from "better-sqlite3"; +import { drizzle } from "drizzle-orm/better-sqlite3"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const state = vi.hoisted(() => ({ + settings: new Map(), + db: null as unknown, +})); + +vi.mock("../../database/db/index.js", () => ({ getDb: () => state.db })); +vi.mock("../../database/repositories/factory.js", () => ({ + createCurrentSettingsRepository: () => ({ + get: async (key: string) => state.settings.get(key) ?? null, + set: async (key: string, value: string) => { + state.settings.set(key, value); + }, + }), +})); +vi.mock("../../utils/logger.js", () => ({ + databaseLogger: { info: vi.fn(), warn: vi.fn() }, +})); + +const { + runHostStatusConfigMigration, + statusConfigFromStatsConfig, + HOST_STATUS_CONFIG_MIGRATED, +} = + await import("../../utils/crypto-migration/host-status-config-migration.js"); + +let sqlite: Database.Database; + +function createHosts(withStatsConfig = true) { + sqlite = new Database(":memory:"); + sqlite.exec(` + CREATE TABLE ssh_data ( + id INTEGER PRIMARY KEY, + ${withStatsConfig ? "stats_config TEXT," : ""} + status_check_enabled INTEGER NOT NULL DEFAULT 1, + status_check_interval INTEGER + ); + `); + state.db = drizzle(sqlite); +} + +const hosts = () => + sqlite + .prepare( + "SELECT id, status_check_enabled AS enabled, status_check_interval AS interval FROM ssh_data ORDER BY id", + ) + .all(); + +beforeEach(() => { + state.settings.clear(); + createHosts(); +}); + +describe("statusConfigFromStatsConfig", () => { + it.each([ + [null, true, null], + ["{}", true, null], + ['{"statusCheckEnabled":false}', false, null], + ['{"disableTcpPing":true}', false, null], + ['{"useGlobalStatusInterval":false,"statusCheckInterval":45}', true, 45], + ['{"useGlobalStatusInterval":true,"statusCheckInterval":45}', true, null], + ['{"useGlobalStatusInterval":false,"statusCheckInterval":2}', true, null], + [ + JSON.stringify(JSON.stringify({ statusCheckEnabled: false })), + false, + null, + ], + ["not json", true, null], + ])("%s", (raw, enabled, interval) => { + expect(statusConfigFromStatsConfig(raw)).toEqual({ + statusCheckEnabled: enabled, + statusCheckInterval: interval, + }); + }); +}); + +describe("runHostStatusConfigMigration", () => { + it("copies each host's status options into the new columns", async () => { + const insert = sqlite.prepare( + "INSERT INTO ssh_data (id, stats_config) VALUES (?, ?)", + ); + insert.run(1, JSON.stringify({ statusCheckEnabled: false })); + insert.run( + 2, + JSON.stringify({ + useGlobalStatusInterval: false, + statusCheckInterval: 45, + }), + ); + insert.run( + 3, + JSON.stringify({ disableTcpPing: true, metricsEnabled: false }), + ); + insert.run(4, JSON.stringify({ metricsInterval: 10 })); + insert.run(5, null); + + const result = await runHostStatusConfigMigration(); + + expect(result).toEqual({ skipped: false, hostsMoved: 3 }); + expect(hosts()).toEqual([ + { id: 1, enabled: 0, interval: null }, + { id: 2, enabled: 1, interval: 45 }, + { id: 3, enabled: 0, interval: null }, + { id: 4, enabled: 1, interval: null }, + { id: 5, enabled: 1, interval: null }, + ]); + expect(state.settings.get(HOST_STATUS_CONFIG_MIGRATED)).toBe("true"); + }); + + it("changes nothing the second time", async () => { + sqlite + .prepare("INSERT INTO ssh_data (id, stats_config) VALUES (?, ?)") + .run(1, JSON.stringify({ statusCheckEnabled: false })); + await runHostStatusConfigMigration(); + + // The user turns checks back on after the upgrade. + sqlite.exec("UPDATE ssh_data SET status_check_enabled = 1 WHERE id = 1"); + expect(await runHostStatusConfigMigration()).toEqual({ + skipped: true, + hostsMoved: 0, + }); + expect(hosts()).toEqual([{ id: 1, enabled: 1, interval: null }]); + }); + + it("finishes on a fresh install that never had stats_config", async () => { + createHosts(false); + sqlite.exec("INSERT INTO ssh_data (id) VALUES (1)"); + expect(await runHostStatusConfigMigration()).toEqual({ + skipped: false, + hostsMoved: 0, + }); + expect(state.settings.get(HOST_STATUS_CONFIG_MIGRATED)).toBe("true"); + }); +}); diff --git a/src/backend/utils/crypto-migration/host-metrics-settings-migration.ts b/src/backend/utils/crypto-migration/host-metrics-settings-migration.ts new file mode 100644 index 000000000..ca04544c6 --- /dev/null +++ b/src/backend/utils/crypto-migration/host-metrics-settings-migration.ts @@ -0,0 +1,182 @@ +/** + * Moves Host Metrics' settings out of core and into the host-metrics + * plugin's own settings. + * + * Admin keys from `settings`: global_metrics_interval, + * metrics_history_retention_days and metricsEnabled inside host_defaults (the + * default for new hosts). Per host, the metrics half of + * ssh_data.stats_config: metricsEnabled, the host's own interval, the enabled + * widgets and the mount lists. The status half is core and moved by + * host-status-config-migration.ts. + * + * The old settings rows and the column stay in place, unused. + * + * Idempotent: an admin key already set, or a host that already has any + * host-metrics row, is skipped, so this is safe on every boot. + */ + +import { sql } from "drizzle-orm"; +import { databaseLogger } from "../logger.js"; +import { + createCurrentPluginRepository, + createCurrentPluginSettingsRepository, + createCurrentSettingsRepository, +} from "../../database/repositories/factory.js"; +import { selectRows } from "./raw-rows.js"; + +const PLUGIN_ID = "host-metrics"; + +/** A JSON object stored as text, or {} when it is anything else. */ +function parseStatsConfig(raw: string | null): Record { + try { + let parsed: unknown = raw ? JSON.parse(raw) : null; + if (typeof parsed === "string") parsed = JSON.parse(parsed); + return parsed && typeof parsed === "object" && !Array.isArray(parsed) + ? (parsed as Record) + : {}; + } catch { + return {}; + } +} + +function seconds(value: unknown, min: number, max: number): number | null { + const parsed = Number(value); + return Number.isInteger(parsed) && parsed >= min && parsed <= max + ? parsed + : null; +} + +/** The plugin's host settings for one stats_config value, defaults left out. */ +export function hostSettingsFromStatsConfig( + raw: string | null, +): Record { + const config = parseStatsConfig(raw); + const values: Record = {}; + if (config.metricsEnabled === false) values.metricsEnabled = false; + if (config.useGlobalMetricsInterval === false) { + const interval = seconds(config.metricsInterval, 5, 3600); + if (interval !== null) values.metricsInterval = interval; + } + if (Array.isArray(config.enabledWidgets)) { + values.enabledWidgets = config.enabledWidgets.filter( + (widget): widget is string => typeof widget === "string", + ); + } + if (Array.isArray(config.excludedMounts) && config.excludedMounts.length) { + values.excludedMounts = config.excludedMounts.filter( + (mount): mount is string => typeof mount === "string", + ); + } + if (Array.isArray(config.monitoredMounts) && config.monitoredMounts.length) { + values.monitoredMounts = config.monitoredMounts.filter( + (mount) => + !!mount && + typeof mount === "object" && + typeof (mount as { path?: unknown }).path === "string", + ); + } + return values; +} + +export interface HostMetricsSettingsMigrationResult { + moved: string[]; + hostsMoved: number; + hostsSkipped: number; +} + +export async function runHostMetricsSettingsMigration(): Promise { + const result: HostMetricsSettingsMigrationResult = { + moved: [], + hostsMoved: 0, + hostsSkipped: 0, + }; + + // plugin_settings has a foreign key to plugins: nothing to migrate into + // until the plugin row exists. + try { + const plugin = await createCurrentPluginRepository().findById(PLUGIN_ID); + if (!plugin) return result; + } catch { + return result; + } + + const pluginSettings = createCurrentPluginSettingsRepository(); + const write = ( + scope: "admin" | "host", + scopeId: string | null, + key: string, + value: unknown, + ) => + pluginSettings.set(PLUGIN_ID, scope, scopeId, key, JSON.stringify(value)); + + try { + const settings = createCurrentSettingsRepository(); + const moves: [string, string, number, number][] = [ + ["global_metrics_interval", "metricsInterval", 5, 3600], + ["metrics_history_retention_days", "historyRetentionDays", 1, 90], + ]; + for (const [legacyKey, field, min, max] of moves) { + const value = seconds(await settings.get(legacyKey), min, max); + if (value === null) continue; + const existing = await pluginSettings.get( + PLUGIN_ID, + "admin", + null, + field, + ); + if (existing && existing.value !== null) continue; + await write("admin", null, field, value); + result.moved.push(legacyKey); + } + + const defaults = parseStatsConfig(await settings.get("host_defaults")); + if (defaults.metricsEnabled === false) { + const existing = await pluginSettings.get( + PLUGIN_ID, + "admin", + null, + "enabledForNewHosts", + ); + if (!existing || existing.value === null) { + await write("admin", null, "enabledForNewHosts", false); + result.moved.push("host_defaults.metricsEnabled"); + } + } + } catch (error) { + databaseLogger.warn("Host Metrics admin settings migration failed", { + operation: "host_metrics_settings_migration", + error: error instanceof Error ? error.message : String(error), + }); + } + + try { + // Raw SQL: schema.ts no longer declares stats_config. + const rows = await selectRows<{ id: number; stats_config: string | null }>( + sql`SELECT id, stats_config FROM ssh_data WHERE stats_config IS NOT NULL`, + ); + for (const row of rows) { + const values = hostSettingsFromStatsConfig(row.stats_config); + if (Object.keys(values).length === 0) continue; + const scopeId = String(row.id); + const existing = await pluginSettings.getAll(PLUGIN_ID, "host", scopeId); + if (existing.length > 0) { + result.hostsSkipped++; + continue; + } + for (const [key, value] of Object.entries(values)) { + await write("host", scopeId, key, value); + } + result.hostsMoved++; + } + } catch { + // A fresh install never had the column. + } + + if (result.moved.length > 0 || result.hostsMoved > 0) { + databaseLogger.info( + `Moved ${result.moved.length} Host Metrics setting(s) and ${result.hostsMoved} host(s) into plugin settings`, + { operation: "host_metrics_settings_migration" }, + ); + } + return result; +} diff --git a/src/backend/utils/crypto-migration/host-status-config-migration.ts b/src/backend/utils/crypto-migration/host-status-config-migration.ts new file mode 100644 index 000000000..1d900bc50 --- /dev/null +++ b/src/backend/utils/crypto-migration/host-status-config-migration.ts @@ -0,0 +1,110 @@ +/** + * Copies the status check options out of ssh_data.stats_config into the + * status_check_enabled and status_check_interval columns. Status checks are + * core; the rest of stats_config belongs to the host-metrics plugin and is + * moved by host-metrics-settings-migration.ts. + * + * The stats_config column stays in the database, unused: core's drizzle + * migrations run before this does, so dropping it there would lose the data. + * + * Runs once, marked by a settings row, because the new columns have defaults + * and there is no other way to tell a copied row from an untouched one. + */ + +import { sql } from "drizzle-orm"; +import { databaseLogger } from "../logger.js"; +import { createCurrentSettingsRepository } from "../../database/repositories/factory.js"; +import { runStatement, selectRows } from "./raw-rows.js"; + +export const HOST_STATUS_CONFIG_MIGRATED = "host_status_config_migrated"; + +export interface LegacyStatusConfig { + statusCheckEnabled: boolean; + statusCheckInterval: number | null; +} + +/** The status part of an old stats_config value. */ +export function statusConfigFromStatsConfig( + raw: string | null, +): LegacyStatusConfig { + let parsed: unknown = null; + try { + parsed = raw ? JSON.parse(raw) : null; + // Some rows were stringified twice. + if (typeof parsed === "string") parsed = JSON.parse(parsed); + } catch { + parsed = null; + } + const config = + parsed && typeof parsed === "object" + ? (parsed as Record) + : {}; + const seconds = Number(config.statusCheckInterval); + return { + statusCheckEnabled: + config.statusCheckEnabled !== false && config.disableTcpPing !== true, + statusCheckInterval: + config.useGlobalStatusInterval === false && + Number.isInteger(seconds) && + seconds >= 5 + ? seconds + : null, + }; +} + +export interface HostStatusConfigMigrationResult { + skipped: boolean; + hostsMoved: number; +} + +export async function runHostStatusConfigMigration(): Promise { + const settings = createCurrentSettingsRepository(); + try { + if ((await settings.get(HOST_STATUS_CONFIG_MIGRATED)) === "true") { + return { skipped: true, hostsMoved: 0 }; + } + } catch { + return { skipped: true, hostsMoved: 0 }; + } + + let hostsMoved = 0; + try { + // Raw SQL: schema.ts no longer declares stats_config. + const rows = await selectRows<{ id: number; stats_config: string | null }>( + sql`SELECT id, stats_config FROM ssh_data WHERE stats_config IS NOT NULL`, + ); + for (const row of rows) { + const config = statusConfigFromStatsConfig(row.stats_config); + if (config.statusCheckEnabled && config.statusCheckInterval === null) { + continue; + } + // TRUE and FALSE as literals: SQLite cannot bind a JS boolean. + await runStatement(sql` + UPDATE ssh_data + SET status_check_enabled = ${sql.raw(config.statusCheckEnabled ? "TRUE" : "FALSE")}, + status_check_interval = ${config.statusCheckInterval} + WHERE id = ${row.id} + `); + hostsMoved++; + } + } catch { + // A fresh install never had the column. + } + + try { + await settings.set(HOST_STATUS_CONFIG_MIGRATED, "true"); + } catch (error) { + databaseLogger.warn("Could not mark the host status migration done", { + operation: "host_status_config_migration", + error: error instanceof Error ? error.message : String(error), + }); + } + + if (hostsMoved > 0) { + databaseLogger.info( + `Moved status check options for ${hostsMoved} host(s) out of stats_config`, + { operation: "host_status_config_migration" }, + ); + } + return { skipped: false, hostsMoved }; +} diff --git a/src/types/index.ts b/src/types/index.ts index 4900d095f..4d4d0125c 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,4 +1,3 @@ -import type { StatsConfig } from "./stats-widgets.js"; import type { Request } from "express"; import type { RefObject } from "react"; import type { HostAuthOverrides } from "./auth-protocols.js"; @@ -230,7 +229,9 @@ export type Host = { tunnelConnections: TunnelConnection[]; jumpHosts?: JumpHost[]; quickActions?: QuickAction[]; - statsConfig?: string | StatsConfig; + statusCheckEnabled?: boolean; + /** Seconds between status checks; null follows the global setting. */ + statusCheckInterval?: number | null; terminalConfig?: Partial; notes?: string; @@ -385,7 +386,9 @@ export interface HostData { tunnelConnections?: TunnelConnection[]; jumpHosts?: JumpHostData[]; quickActions?: QuickActionData[]; - statsConfig?: string | StatsConfig; + statusCheckEnabled?: boolean; + /** Seconds between status checks; null follows the global setting. */ + statusCheckInterval?: number | null; terminalConfig?: Partial; notes?: string; diff --git a/src/types/ui-types.ts b/src/types/ui-types.ts index 2add9f67e..b7b96dc3c 100644 --- a/src/types/ui-types.ts +++ b/src/types/ui-types.ts @@ -1,5 +1,4 @@ import type { TerminalConfig } from "./index.js"; -import type { StatsConfig } from "./stats-widgets.js"; import type { HostAuthOverrides } from "./auth-protocols.js"; export type Host = { @@ -147,7 +146,9 @@ export type Host = { enabledCards?: string[]; } | null; - statsConfig?: StatsConfig; + statusCheckEnabled?: boolean; + /** Seconds between status checks; null follows the global setting. */ + statusCheckInterval?: number | null; quickActions: { name: string; snippetId: string }[]; enableSsh: boolean; diff --git a/src/ui/api/host-metrics-status-api.ts b/src/ui/api/host-metrics-status-api.ts deleted file mode 100644 index 9ad3ce9d1..000000000 --- a/src/ui/api/host-metrics-status-api.ts +++ /dev/null @@ -1,347 +0,0 @@ -import axios, { type AxiosRequestConfig } from "axios"; -import { - handleApiError, - statsApi, - getRemoteStatsApi, - isElectron, - sshHostApi, -} from "@/main-axios"; -import type { ServerMetrics, ServerStatus } from "@/main-axios"; -import { getCachedServerStatuses } from "@/lib/hosts-request-cache"; -import { resolveConnectionOrigin } from "@/lib/connection-origin"; -import type { SSHHost } from "@/types/index"; -import { createKeyedRequestCache } from "@/lib/keyed-request-cache"; - -// Metrics collection/viewer registration below (startMetricsPolling, -// registerMetricsViewer, etc.) is NOT origin-routed: the backend that -// receives the call must own the target host by numeric database id, and a -// synced host has a different numeric id in each database (only its -// syncId matches across them). Only the aggregate status read is merged -// across local + remote, same as tunnel status. -async function isRemoteSyncConnected(): Promise { - if (!isElectron()) return false; - try { - const config = (await window.electronAPI?.invoke?.( - "get-remote-sync-config", - )) as { serverUrl?: string } | null; - return !!config?.serverUrl; - } catch { - return false; - } -} - -type ApiConnectionLog = { - type: "info" | "success" | "warning" | "error"; - stage: string; - message: string; - details?: Record; -}; - -type ConnectErrorResponse = { - error?: string; - message?: string; - connectionLogs?: ApiConnectionLog[]; - requires_totp?: boolean; - requires_warpgate?: boolean; - sessionId?: string; - prompt?: string; - url?: string; - securityKey?: string; - status?: string; - reason?: string; -}; - -// SERVER STATISTICS -// ============================================================================ - -const metricsCache = createKeyedRequestCache(1_500, 100); - -/** - * Progressive retry schedule for the background /status poll. - * - * Each entry describes one attempt's per-request timeout and the pause to - * observe before the next attempt. The pause on the last entry is `null`: - * after that final failure we surface the network error, which flows - * through the response interceptor + dbHealthMonitor (which decides - * between the degraded toast and the full-outage overlay based on whether - * any WebSocket is still alive). - * - * Sequence: try(2s) -> wait 3s -> try(5s) -> wait 5s -> try(8s) -> fail. - * Worst-case wall-clock = 23s, which fits inside the 30s ServerStatusContext - * poll cadence, so the next tick acts as the next retry without overlap. - */ -const STATUS_RETRY_SCHEDULE: ReadonlyArray<{ - timeoutMs: number; - pauseAfterMs: number | null; -}> = [ - { timeoutMs: 2000, pauseAfterMs: 3000 }, - { timeoutMs: 5000, pauseAfterMs: 5000 }, - { timeoutMs: 8000, pauseAfterMs: null }, -]; - -function isTransientStatusError(error: unknown): boolean { - if (!axios.isAxiosError(error)) return false; - if (error.response) { - // Definitive server response (even 5xx) is not something more retries - // will fix in a useful timeframe; bail out and report it normally. - return false; - } - const code = error.code; - if (!code) { - // No code + no response means classic network error (offline / DNS / TCP) - return true; - } - return ( - code === "ECONNABORTED" || - code === "ETIMEDOUT" || - code === "ERR_NETWORK" || - code === "ECONNREFUSED" || - code === "ECONNRESET" - ); -} - -export async function getAllServerStatuses(): Promise< - Record -> { - return getCachedServerStatuses(async () => { - let lastError: unknown = null; - let localStatuses: Record = {}; - let localHostIds: number[] | null = null; - - if (isElectron()) { - try { - const response = await sshHostApi.get("/db/host"); - const defaultOrigin = await resolveConnectionOrigin({ - connectionOrigin: null, - }); - localHostIds = (response.data || []) - .filter( - (host) => (host.connectionOrigin ?? defaultOrigin) === "local", - ) - .map((host) => host.id); - } catch { - // A host-list failure must not start local probes for hosts whose - // configured origin may be remote. - localHostIds = []; - } - } - - for (let i = 0; i < STATUS_RETRY_SCHEDULE.length; i++) { - const { timeoutMs, pauseAfterMs } = STATUS_RETRY_SCHEDULE[i]; - const isFinalAttempt = i === STATUS_RETRY_SCHEDULE.length - 1; - - try { - const response = await statsApi.get("/status", { - timeout: timeoutMs, - ...(localHostIds === null - ? {} - : { params: { hostIds: localHostIds.join(",") } }), - // Silence per-attempt interceptor logging & health-monitor side - // effects on all attempts except the final one, so background - // blips don't look like real outages. - __silentRetry: !isFinalAttempt, - } as AxiosRequestConfig & { __silentRetry?: boolean }); - localStatuses = response.data || {}; - lastError = null; - break; - } catch (error) { - lastError = error; - if (!isTransientStatusError(error)) { - break; - } - if (pauseAfterMs === null) { - break; - } - await new Promise((resolve) => setTimeout(resolve, pauseAfterMs)); - } - } - - if (lastError) { - handleApiError(lastError, "fetch server statuses"); - return {}; - } - - if (await isRemoteSyncConnected()) { - try { - const remoteResult = await getRemoteStatsApi().get("/status", { - timeout: 8000, - __silentRetry: true, - } as AxiosRequestConfig & { __silentRetry?: boolean }); - return { ...localStatuses, ...(remoteResult.data || {}) }; - } catch { - // remote unreachable this tick -- fall back to local-only statuses - } - } - - return localStatuses; - }); -} - -export async function getServerStatusById(id: number): Promise { - try { - const response = await statsApi.get(`/status/${id}`); - return response.data; - } catch (error) { - handleApiError(error, "fetch server status"); - throw error; - } -} - -export async function getServerMetricsById( - id: number, -): Promise { - return metricsCache.get(String(id), async () => { - try { - const response = await statsApi.get(`/metrics/${id}`, { - // Treat 404 as an expected "no metrics yet / disabled" signal rather - // than an error so we don't spam warn logs on the client. - validateStatus: (status) => status === 200 || status === 404, - }); - if (response.status === 404) return null; - return response.data; - } catch (error) { - // If a 404 still slips through (e.g. intercepted before reaching here), - // swallow it quietly; everything else still flows through handleApiError. - if (axios.isAxiosError(error) && error.response?.status === 404) { - return null; - } - handleApiError(error, "fetch server metrics"); - } - }); -} - -export async function startMetricsPolling(hostId: number): Promise<{ - success: boolean; - requires_totp?: boolean; - sessionId?: string; - prompt?: string; - viewerSessionId?: string; - connectionLogs?: ApiConnectionLog[]; -}> { - try { - const response = await statsApi.post(`/metrics/start/${hostId}`); - metricsCache.invalidate(String(hostId)); - return response.data; - } catch (error: unknown) { - if ( - axios.isAxiosError(error) && - error.response?.data?.connectionLogs - ) { - const data = error.response.data; - const errorWithLogs = new Error( - data.error || data.message || error.message, - ); - Object.assign(errorWithLogs, { - connectionLogs: data.connectionLogs, - }); - throw errorWithLogs; - } - handleApiError(error, "start metrics polling"); - throw error; - } -} - -export async function stopMetricsPolling( - hostId: number, - viewerSessionId?: string, -): Promise { - try { - await statsApi.post(`/metrics/stop/${hostId}`, { viewerSessionId }); - } catch (error) { - handleApiError(error, "stop metrics polling"); - throw error; - } -} - -export async function sendMetricsHeartbeat( - viewerSessionId: string, -): Promise { - try { - const response = await statsApi.post( - "/metrics/heartbeat", - { viewerSessionId }, - { validateStatus: (status) => status === 200 || status === 404 }, - ); - return response.status !== 404; - } catch (error) { - if (axios.isAxiosError(error) && error.response?.status === 404) { - return false; - } - handleApiError(error, "send metrics heartbeat"); - throw error; - } -} - -export async function registerMetricsViewer(hostId: number): Promise<{ - success: boolean; - viewerSessionId?: string; - skipped?: boolean; - reason?: string; -}> { - try { - const response = await statsApi.post("/metrics/register-viewer", { - hostId, - }); - metricsCache.invalidate(String(hostId)); - return response.data; - } catch (error) { - handleApiError(error, "register metrics viewer"); - throw error; - } -} - -export async function unregisterMetricsViewer( - hostId: number, - viewerSessionId: string, -): Promise { - try { - await statsApi.post("/metrics/unregister-viewer", { - hostId, - viewerSessionId, - }); - } catch (error) { - handleApiError(error, "unregister metrics viewer"); - throw error; - } -} - -export async function submitMetricsTOTP( - sessionId: string, - totpCode: string, -): Promise<{ - success: boolean; - viewerSessionId?: string; -}> { - try { - const response = await statsApi.post("/metrics/connect-totp", { - sessionId, - totpCode, - }); - metricsCache.invalidate(); - return response.data; - } catch (error) { - handleApiError(error, "submit metrics TOTP"); - throw error; - } -} - -export async function refreshServerPolling(): Promise { - try { - await statsApi.post("/refresh"); - } catch (error) { - console.warn("Failed to refresh server polling:", error); - } -} - -export async function notifyHostCreatedOrUpdated( - hostId: number, -): Promise { - try { - await statsApi.post("/host-updated", { hostId }); - metricsCache.invalidate(String(hostId)); - } catch (error) { - console.warn("Failed to notify stats server of host update:", error); - } -} - -// ============================================================================ diff --git a/src/ui/api/host-status-api.ts b/src/ui/api/host-status-api.ts new file mode 100644 index 000000000..38c0acf32 --- /dev/null +++ b/src/ui/api/host-status-api.ts @@ -0,0 +1,190 @@ +import axios, { type AxiosRequestConfig } from "axios"; +import { + handleApiError, + getRemoteCoreApi, + isElectron, + sshHostApi, +} from "@/main-axios"; +import type { ServerStatus } from "@/main-axios"; +import { getCachedServerStatuses } from "@/lib/hosts-request-cache"; +import { resolveConnectionOrigin } from "@/lib/connection-origin"; +import type { SSHHost } from "@/types/index"; + +// Core's host status checks (/host/status). On the desktop app the aggregate +// read is merged across the local backend and the connected remote server. +async function isRemoteSyncConnected(): Promise { + if (!isElectron()) return false; + try { + const config = (await window.electronAPI?.invoke?.( + "get-remote-sync-config", + )) as { serverUrl?: string } | null; + return !!config?.serverUrl; + } catch { + return false; + } +} + +// HOST STATUS +// ============================================================================ + +/** + * Progressive retry schedule for the background /status poll. + * + * Each entry describes one attempt's per-request timeout and the pause to + * observe before the next attempt. The pause on the last entry is `null`: + * after that final failure we surface the network error, which flows + * through the response interceptor + dbHealthMonitor (which decides + * between the degraded toast and the full-outage overlay based on whether + * any WebSocket is still alive). + * + * Sequence: try(2s) -> wait 3s -> try(5s) -> wait 5s -> try(8s) -> fail. + * Worst-case wall-clock = 23s, which fits inside the 30s ServerStatusContext + * poll cadence, so the next tick acts as the next retry without overlap. + */ +const STATUS_RETRY_SCHEDULE: ReadonlyArray<{ + timeoutMs: number; + pauseAfterMs: number | null; +}> = [ + { timeoutMs: 2000, pauseAfterMs: 3000 }, + { timeoutMs: 5000, pauseAfterMs: 5000 }, + { timeoutMs: 8000, pauseAfterMs: null }, +]; + +function isTransientStatusError(error: unknown): boolean { + if (!axios.isAxiosError(error)) return false; + if (error.response) { + // Definitive server response (even 5xx) is not something more retries + // will fix in a useful timeframe; bail out and report it normally. + return false; + } + const code = error.code; + if (!code) { + // No code + no response means classic network error (offline / DNS / TCP) + return true; + } + return ( + code === "ECONNABORTED" || + code === "ETIMEDOUT" || + code === "ERR_NETWORK" || + code === "ECONNREFUSED" || + code === "ECONNRESET" + ); +} + +export async function getAllServerStatuses(): Promise< + Record +> { + return getCachedServerStatuses(async () => { + let lastError: unknown = null; + let localStatuses: Record = {}; + let localHostIds: number[] | null = null; + + if (isElectron()) { + try { + const response = await sshHostApi.get("/db/host"); + const defaultOrigin = await resolveConnectionOrigin({ + connectionOrigin: null, + }); + localHostIds = (response.data || []) + .filter( + (host) => (host.connectionOrigin ?? defaultOrigin) === "local", + ) + .map((host) => host.id); + } catch { + // A host-list failure must not start local probes for hosts whose + // configured origin may be remote. + localHostIds = []; + } + } + + for (let i = 0; i < STATUS_RETRY_SCHEDULE.length; i++) { + const { timeoutMs, pauseAfterMs } = STATUS_RETRY_SCHEDULE[i]; + const isFinalAttempt = i === STATUS_RETRY_SCHEDULE.length - 1; + + try { + const response = await sshHostApi.get("/status", { + timeout: timeoutMs, + ...(localHostIds === null + ? {} + : { params: { hostIds: localHostIds.join(",") } }), + // Silence per-attempt interceptor logging & health-monitor side + // effects on all attempts except the final one, so background + // blips don't look like real outages. + __silentRetry: !isFinalAttempt, + } as AxiosRequestConfig & { __silentRetry?: boolean }); + localStatuses = response.data || {}; + lastError = null; + break; + } catch (error) { + lastError = error; + if (!isTransientStatusError(error)) { + break; + } + if (pauseAfterMs === null) { + break; + } + await new Promise((resolve) => setTimeout(resolve, pauseAfterMs)); + } + } + + if (lastError) { + handleApiError(lastError, "fetch server statuses"); + return {}; + } + + if (await isRemoteSyncConnected()) { + try { + const remoteResult = await getRemoteCoreApi().get("/host/status", { + timeout: 8000, + __silentRetry: true, + } as AxiosRequestConfig & { __silentRetry?: boolean }); + return { ...localStatuses, ...(remoteResult.data || {}) }; + } catch { + // remote unreachable this tick -- fall back to local-only statuses + } + } + + return localStatuses; + }); +} + +export async function getServerStatusById(id: number): Promise { + try { + const response = await sshHostApi.get(`/status/${id}`); + return response.data; + } catch (error) { + handleApiError(error, "fetch server status"); + throw error; + } +} + +export async function refreshServerPolling(): Promise { + try { + await sshHostApi.post("/status/refresh"); + } catch (error) { + console.warn("Failed to refresh status checks:", error); + } +} + +export async function getStatusCheckSettings(): Promise<{ + statusCheckInterval: number; +}> { + try { + const response = await sshHostApi.get("/status/settings"); + return response.data; + } catch (error) { + handleApiError(error, "fetch status check settings"); + } +} + +export async function updateStatusCheckSettings(settings: { + statusCheckInterval: number; +}): Promise { + try { + await sshHostApi.put("/status/settings", settings); + } catch (error) { + handleApiError(error, "update status check settings"); + } +} + +// ============================================================================ diff --git a/src/ui/api/settings-api.ts b/src/ui/api/settings-api.ts index ea017d716..bb428fc6e 100644 --- a/src/ui/api/settings-api.ts +++ b/src/ui/api/settings-api.ts @@ -1,32 +1,5 @@ -import { authApi, handleApiError, statsApi } from "@/main-axios"; +import { authApi, handleApiError } from "@/main-axios"; -// GLOBAL MONITORING SETTINGS -// ============================================================================ - -export async function getGlobalMonitoringSettings(): Promise<{ - statusCheckInterval: number; - metricsInterval: number; -}> { - try { - const response = await statsApi.get("/global-settings"); - return response.data; - } catch (error) { - handleApiError(error, "fetch global monitoring settings"); - } -} - -export async function updateGlobalMonitoringSettings(settings: { - statusCheckInterval?: number; - metricsInterval?: number; -}): Promise { - try { - await statsApi.post("/global-settings", settings); - } catch (error) { - handleApiError(error, "update global monitoring settings"); - } -} - -// ============================================================================ // LOG LEVEL SETTINGS // ============================================================================ @@ -184,7 +157,6 @@ export type HostDefaults = { socks5Username?: string; socks5Password?: string; credentialId?: number | null; - metricsEnabled?: boolean; statusCheckEnabled?: boolean; fontSize?: number; fontFamily?: string; diff --git a/src/ui/dashboard/DashboardTab.tsx b/src/ui/dashboard/DashboardTab.tsx index bbd0e7b18..7796011e3 100644 --- a/src/ui/dashboard/DashboardTab.tsx +++ b/src/ui/dashboard/DashboardTab.tsx @@ -1,6 +1,6 @@ import { useState, useRef, useCallback, useEffect } from "react"; import { enabledHostProtocols, protocolPort } from "@/sidebar/host-protocols"; -import { useSshAuthProviders } from "@/hooks/useSshAuthProviders"; +import { ComponentSlot } from "@/shell/ActionSlot"; import { useIsMobile } from "@/hooks/use-mobile"; import { Button } from "@/components/button"; import { Card } from "@/components/card"; @@ -38,10 +38,6 @@ import { getRecentActivity, getCredentials, resetRecentActivity, - getAllServerStatuses, - getServerMetricsById, - registerMetricsViewer, - sendMetricsHeartbeat, getUserInfo, getServiceLinks, createServiceLink, @@ -446,45 +442,12 @@ function QuickActionsCard({ ); } -function MetricBar({ label, value }: { label: string; value: number }) { - const color = - value >= 90 - ? "bg-red-500" - : value >= 70 - ? "bg-yellow-500" - : "bg-accent-brand"; - const textColor = - value >= 90 - ? "text-red-400" - : value >= 70 - ? "text-yellow-400" - : "text-accent-brand"; - return ( -
-
- {label} - - {value.toFixed(0)}% - -
-
-
-
-
- ); -} - export function HostStatusCard({ hosts, - hostMetrics, onOpenTab, statusLoading, }: { hosts: Host[]; - hostMetrics: Map< - string, - { cpu: number | null; ram: number | null; disk: number | null } - >; onOpenTab: (host: Host, type: TabType) => void; statusLoading?: boolean; }) { @@ -518,11 +481,6 @@ export function HostStatusCard({ : host.online ? "online" : "offline"; - const metrics = hostMetrics.get(host.id); - const cpu = metrics?.cpu ?? null; - const ram = metrics?.ram ?? null; - const disk = metrics?.disk ?? null; - const hasMetrics = cpu !== null || ram !== null || disk !== null; return (
- {availability === "online" && hasMetrics ? ( -
- {cpu !== null && ( - - )} - {ram !== null && ( - - )} - {disk !== null && ( - - )} -
- ) : ( -
- - — - - - — - - - — - -
- )} + {/* Plugins such as host metrics show live usage here. */} + @@ -609,7 +550,7 @@ export function HostStatusCard({ } function isStatusCheckEnabled(host: Host): boolean { - return host.statsConfig?.statusCheckEnabled !== false; + return host.statusCheckEnabled !== false; } function RecentActivityCard({ @@ -843,7 +784,6 @@ function CardItem({ onOpenSingletonTab, onOpenTab, hosts, - hostMetrics, uptimeFormatted, versionText, versionStatus, @@ -871,10 +811,6 @@ function CardItem({ onOpenSingletonTab: (type: TabType, pendingEvent?: string) => void; onOpenTab: (host: Host, type: TabType) => void; hosts: Host[]; - hostMetrics: Map< - string, - { cpu: number | null; ram: number | null; disk: number | null } - >; uptimeFormatted: string; versionText: string; versionStatus: "up_to_date" | "requires_update" | "beta" | "unknown"; @@ -970,7 +906,6 @@ function CardItem({ {slot.id === "host_status" && ( @@ -1103,10 +1038,6 @@ type PanelColumnProps = { onOpenSingletonTab: (type: TabType, pendingEvent?: string) => void; onOpenTab: (host: Host, type: TabType) => void; hosts: Host[]; - hostMetrics: Map< - string, - { cpu: number | null; ram: number | null; disk: number | null } - >; uptimeFormatted: string; versionText: string; versionStatus: "up_to_date" | "requires_update" | "beta" | "unknown"; @@ -1139,7 +1070,6 @@ function PanelColumn({ onOpenSingletonTab, onOpenTab, hosts, - hostMetrics, uptimeFormatted, versionText, versionStatus, @@ -1198,7 +1128,6 @@ function PanelColumn({ onOpenSingletonTab={onOpenSingletonTab} onOpenTab={onOpenTab} hosts={hosts} - hostMetrics={hostMetrics} uptimeFormatted={uptimeFormatted} versionText={versionText} versionStatus={versionStatus} @@ -1378,83 +1307,13 @@ export function DashboardTab({ const [credentialCount, setCredentialCount] = useState(0); const [activeTunnelCount, setActiveTunnelCount] = useState(0); const [activity, setActivity] = useState([]); - const [hostMetrics, setHostMetrics] = useState< - Map - >(new Map()); - const viewerSessionsRef = useRef>(new Map()); - const sshAuthProviders = useSshAuthProviders(); - const sshAuthProvidersRef = useRef(sshAuthProviders); - sshAuthProvidersRef.current = sshAuthProviders; const statusCheckHosts = hosts.filter(isStatusCheckEnabled); - const fetchMetrics = useCallback(async (hostList: Host[]) => { - let statuses: Record = {}; - try { - statuses = (await getAllServerStatuses()) as Record< - number, - { status?: string } - >; - } catch { - /* best-effort */ - } - - const newSessions = new Map(viewerSessionsRef.current); - const results = await Promise.all( - hostList.map(async (host) => { - const hostId = Number(host.id); - const knownStatus = statuses?.[hostId]?.status; - if (knownStatus === "offline") return null; - // Types that need a person or a browser sign-in cannot be polled. - const authOption = sshAuthProvidersRef.current.find(host.authType); - if (authOption && !authOption.supportsBackground) return null; - - try { - const existing = newSessions.get(hostId); - if (existing && !(await sendMetricsHeartbeat(existing))) { - newSessions.delete(hostId); - } - if (!newSessions.has(hostId)) { - const reg = await registerMetricsViewer(hostId); - if (reg.skipped) return null; - if (reg.success && reg.viewerSessionId) { - newSessions.set(hostId, reg.viewerSessionId); - } - } - const metrics = await getServerMetricsById(hostId); - if (!metrics) return null; - return { - id: host.id, - cpu: metrics.cpu?.percent ?? null, - ram: metrics.memory?.percent ?? null, - disk: metrics.disk?.percent ?? null, - }; - } catch { - return null; - } - }), - ); - - viewerSessionsRef.current = newSessions; - const map = new Map< - string, - { cpu: number | null; ram: number | null; disk: number | null } - >(); - for (const r of results) { - if (r) map.set(r.id, { cpu: r.cpu, ram: r.ram, disk: r.disk }); - } - setHostMetrics(map); - }, []); - useEffect(() => { let mounted = true; const load = async () => { const raw = await getSSHHosts().catch(() => []); - const mapped = raw.map(sshHostToHost); - const statusHosts = mapped.filter(isStatusCheckEnabled); - if (mounted) setHosts(mapped); - if (isVisible) { - fetchMetrics(statusHosts).catch(() => {}); - } + if (mounted) setHosts(raw.map(sshHostToHost)); }; load(); @@ -1518,37 +1377,17 @@ export function DashboardTab({ }; } - const metricsInterval = setInterval(async () => { + const hostsInterval = setInterval(async () => { if (document.visibilityState === "hidden") return; const raw = await getSSHHosts().catch(() => []); - const mapped = raw.map(sshHostToHost); - const statusHosts = mapped.filter(isStatusCheckEnabled); - if (mounted) setHosts(mapped); - fetchMetrics(statusHosts).catch(() => {}); + if (mounted) setHosts(raw.map(sshHostToHost)); }, 30000); return () => { mounted = false; - clearInterval(metricsInterval); + clearInterval(hostsInterval); }; - }, [fetchMetrics, isVisible]); - - useEffect(() => { - if (!isVisible || viewerSessionsRef.current.size === 0) return; - const heartbeat = setInterval(async () => { - if (document.visibilityState === "hidden") return; - for (const [hostId, sessionId] of viewerSessionsRef.current) { - sendMetricsHeartbeat(sessionId) - .then((alive) => { - if (!alive && viewerSessionsRef.current.get(hostId) === sessionId) { - viewerSessionsRef.current.delete(hostId); - } - }) - .catch(() => {}); - } - }, 30000); - return () => clearInterval(heartbeat); - }, [hostMetrics, isVisible]); + }, [isVisible]); const handleClearActivity = async () => { try { @@ -1705,7 +1544,6 @@ export function DashboardTab({ const columnProps = { hosts, - hostMetrics, uptimeFormatted, versionText, versionStatus, @@ -1847,7 +1685,6 @@ export function DashboardTab({ {slot.id === "host_status" && ( diff --git a/src/ui/features/homepage/dialogs/HostStatusEditForm.tsx b/src/ui/features/homepage/dialogs/HostStatusEditForm.tsx index 21798bd8a..c86f243c7 100644 --- a/src/ui/features/homepage/dialogs/HostStatusEditForm.tsx +++ b/src/ui/features/homepage/dialogs/HostStatusEditForm.tsx @@ -24,21 +24,6 @@ const METRIC_OPTIONS: { key: HostMetricKey; labelKey: string }[] = [ { key: "processes", labelKey: "homepage.metricProcesses" }, ]; -function parseMetricsEnabled(statsConfig: unknown): boolean { - if (!statsConfig) return false; - try { - const cfg = - typeof statsConfig === "string" ? JSON.parse(statsConfig) : statsConfig; - if (typeof cfg === "object" && cfg !== null) { - const c = cfg as Record; - return c.metricsEnabled !== false; - } - } catch { - // ignore - } - return false; -} - export function HostStatusEditForm({ config, onChange, @@ -57,7 +42,7 @@ export function HostStatusEditForm({ .then((all) => setHosts( all - .filter((x) => parseMetricsEnabled(x.statsConfig)) + .filter((x) => x.statusCheckEnabled !== false) .map((x) => ({ id: String(x.id), name: x.name, diff --git a/src/ui/features/homepage/widgets/HostStatusWidget.tsx b/src/ui/features/homepage/widgets/HostStatusWidget.tsx index 202dc87da..80b93f892 100644 --- a/src/ui/features/homepage/widgets/HostStatusWidget.tsx +++ b/src/ui/features/homepage/widgets/HostStatusWidget.tsx @@ -8,12 +8,9 @@ import type { WidgetComponentProps, } from "@/types/homepage-types"; import { GRID_SIZE } from "@/types/homepage-types"; -import { - getServerStatusById, - getServerMetricsById, -} from "@/api/host-metrics-status-api"; +import { getServerStatusById } from "@/api/host-status-api"; import { getSSHHosts } from "@/api/ssh-host-management-api"; -import type { ServerMetrics } from "@/main-axios"; +import { ComponentSlot } from "@/shell/ActionSlot"; import { WidgetTitle } from "./WidgetTitle"; function getAccentColor(): string { @@ -34,56 +31,6 @@ function migrateConfig(config: HostStatusConfig): HostMetricKey[] { return metrics; } -interface MetricBarProps { - label: string; - value: number | null; - sublabel?: string; -} - -function MetricBar({ label, value, sublabel }: MetricBarProps) { - const pct = value ?? 0; - const color = pct > 90 ? "#ef4444" : pct > 70 ? "#f97316" : getAccentColor(); - return ( -
-
- {label} - - {value != null ? `${Math.round(pct)}%` : "N/A"} - {sublabel && {sublabel}} - -
-
-
-
-
- ); -} - -interface InfoRowProps { - label: string; - value: string | null | undefined; -} - -function InfoRow({ label, value }: InfoRowProps) { - if (!value) return null; - return ( -
- {label} - {value} -
- ); -} - -function formatBytes(n: number): string { - if (n >= 1073741824) return `${(n / 1073741824).toFixed(1)} GB`; - if (n >= 1048576) return `${(n / 1048576).toFixed(1)} MB`; - if (n >= 1024) return `${(n / 1024).toFixed(1)} KB`; - return `${n} B`; -} - function HostStatusWidget({ widget, config, @@ -94,7 +41,6 @@ function HostStatusWidget({ const [hostName, setHostName] = useState(null); const [online, setOnline] = useState(null); const [reachable, setReachable] = useState(false); - const [metrics, setMetrics] = useState(null); const needsMetrics = shownMetrics.length > 0; @@ -125,15 +71,6 @@ function HostStatusWidget({ setReachable(false); } } - - if (needsMetrics) { - try { - const m = await getServerMetricsById(hostId); - if (!cancelled) setMetrics(m); - } catch { - if (!cancelled) setMetrics(null); - } - } }; poll(); @@ -141,7 +78,6 @@ function HostStatusWidget({ let intervalId: ReturnType | null = null; const start = () => { if (intervalId !== null) return; - // Align with global status cadence; metrics do not need sub-10s when hidden. intervalId = setInterval(() => { if (document.visibilityState === "hidden") return; void poll(); @@ -169,7 +105,7 @@ function HostStatusWidget({ stop(); document.removeEventListener("visibilitychange", onVisibility); }; - }, [hostId, needsMetrics]); + }, [hostId]); if (!hostId) { return ( @@ -196,16 +132,6 @@ function HostStatusWidget({ ? t("common.reachable", { defaultValue: "Reachable" }) : t("common.offline"); - const networkIfaces = metrics?.network?.interfaces ?? []; - const totalRx = networkIfaces.reduce((sum, iface) => { - const raw = iface.rxBytes ?? iface.rx ?? null; - return raw ? sum + parseFloat(raw) : sum; - }, 0); - const totalTx = networkIfaces.reduce((sum, iface) => { - const raw = iface.txBytes ?? iface.tx ?? null; - return raw ? sum + parseFloat(raw) : sum; - }, 0); - return (
} /> @@ -231,132 +157,12 @@ function HostStatusWidget({
+ {/* Plugins such as host metrics fill in the live numbers. */} {needsMetrics && ( -
- {shownMetrics.includes("cpu") && ( - - )} - - {shownMetrics.includes("memory") && ( - - )} - - {shownMetrics.includes("disk") && ( - - )} - - {shownMetrics.includes("uptime") && ( - - )} - - {shownMetrics.includes("system") && metrics?.system && ( -
- - - -
- )} - - {shownMetrics.includes("network") && networkIfaces.length > 0 && ( -
-
- {t("homepage.metricNetwork")} -
- {networkIfaces.slice(0, 3).map((iface) => ( -
- - {iface.name} - - - {iface.ip || iface.state} - -
- ))} - {(totalRx > 0 || totalTx > 0) && ( -
- RX/TX - - {formatBytes(totalRx)} / {formatBytes(totalTx)} - -
- )} -
- )} - - {shownMetrics.includes("processes") && metrics?.processes && ( -
-
- - {t("homepage.metricProcesses")} - - - {metrics.processes.total ?? "?"}{" "} - {t("homepage.metricProcessesTotal")} - {metrics.processes.running != null - ? `, ${metrics.processes.running} ${t("homepage.metricProcessesRunning")}` - : ""} - -
- {metrics.processes.top?.slice(0, 3).map((proc) => ( -
- - {proc.command} - - - {proc.cpu}% - -
- ))} -
- )} - - {needsMetrics && !metrics && online && ( - - {t("homepage.metricsNotAvailable")} - - )} -
+ )}
diff --git a/src/ui/lib/ServerStatusContext.tsx b/src/ui/lib/ServerStatusContext.tsx index 43127ba3f..99f437635 100644 --- a/src/ui/lib/ServerStatusContext.tsx +++ b/src/ui/lib/ServerStatusContext.tsx @@ -10,7 +10,6 @@ import React, { useState, } from "react"; import { getAllServerStatuses, getSSHHosts } from "@/main-axios"; -import { DEFAULT_STATS_CONFIG } from "@/types/stats-widgets"; import { ServerStatusStore, type ServerStatusEntry, @@ -73,21 +72,7 @@ export function ServerStatusProvider({ const enabled = new Set(); hosts.forEach((host) => { - const statsConfig = (() => { - try { - if (!host.statsConfig) return DEFAULT_STATS_CONFIG; - if (typeof host.statsConfig === "string") { - return JSON.parse(host.statsConfig); - } - return host.statsConfig; - } catch { - return DEFAULT_STATS_CONFIG; - } - })(); - - if (statsConfig.statusCheckEnabled !== false) { - enabled.add(host.id); - } + if (host.statusCheckEnabled !== false) enabled.add(host.id); }); store.setEnabledHostIds(enabled); @@ -322,3 +307,30 @@ export function useServerStatusMeta(): { isLoading: store.getIsLoading(), }; } + +const noStatus = () => ""; + +/** + * One host's status for plugin views, which can render outside the app shell + * (a standalone window). Null there, and while the host has not been checked. + */ +export function useOptionalHostStatusEntry(hostId: number | undefined): { + status: StatusValue; + reason?: "host_key_changed"; +} | null { + const store = useContext(StatusStoreContext); + const snapshot = useSyncExternalStore( + (onChange) => + store && hostId !== undefined + ? store.subscribeHost(hostId, onChange) + : () => {}, + () => (store && hostId !== undefined ? store.getHostSnapshot(hostId) : ""), + noStatus, + ); + if (!snapshot) return null; + const [status] = snapshot.split(":", 1); + if (!status) return null; + return snapshot.endsWith(":host_key_changed") + ? { status: status as StatusValue, reason: "host_key_changed" } + : { status: status as StatusValue }; +} diff --git a/src/ui/locales/en.json b/src/ui/locales/en.json index af4c29378..0b7a70c5c 100644 --- a/src/ui/locales/en.json +++ b/src/ui/locales/en.json @@ -268,7 +268,6 @@ "showDisk": "Show Disk Usage", "addToFolder": "Add widget to folder", "noHostSelected": "No host selected", - "metricsNotAvailable": "Metrics not available", "selectHost": "Select a host...", "displayedMetrics": "Displayed Metrics", "metricCpu": "CPU", @@ -276,13 +275,8 @@ "metricDisk": "Disk", "metricUptime": "Uptime", "metricSystem": "System", - "metricOs": "OS", - "metricKernel": "Kernel", - "metricHostname": "Hostname", "metricNetwork": "Network", "metricProcesses": "Processes", - "metricProcessesTotal": "total", - "metricProcessesRunning": "running", "categoryMonitoring": "Monitoring", "loading": "Loading...", "allClear": "All clear", @@ -771,10 +765,6 @@ "discardChanges": "Discard changes", "movedToFolder": "Moved {{count}} host(s) to \"{{folder}}\"", "editHostTooltip": "Edit host", - "statusChecks": "Status Checks", - "metricsCollection": "Metrics Collection", - "metricsInterval": "Metrics Collection Interval", - "metricsIntervalDesc": "How often to collect server statistics (5s - 1h)", "behavior": "Behavior", "themePreview": "Theme Preview", "theme": "Theme", @@ -1907,8 +1897,6 @@ "serverStats": "Server Stats", "loadingServerStats": "Loading server stats...", "noServerData": "No server data available", - "cpu": "CPU", - "ram": "RAM", "customizeLayout": "Customize Dashboard", "dashboardSettings": "Dashboard Settings", "enableDisableCards": "Enable/Disable Cards", @@ -1961,7 +1949,6 @@ "serviceLinksUrlPlaceholder": "http://192.168.1.10:8080", "serviceLinksInvalidUrl": "Enter a valid web address", "serviceLinksAddFailed": "Failed to add service link", - "disk": "Disk", "viewServerDetails": "View server details" }, "admin": { @@ -2102,7 +2089,6 @@ "sessionTimeoutRange": "Min 1h · Max 720h", "monitoringDefaults": "Monitoring Defaults", "statusCheck": "Status Check", - "metrics": "Metrics", "sec": "sec", "logLevel": "Log Level", "oidcDescription": "Configure OpenID Connect for SSO. Fields marked * are required.", @@ -2352,11 +2338,9 @@ "hostDefaultsSocks5Host": "Proxy Host / Port", "hostDefaultsSocks5Username": "Proxy Username", "hostDefaultsSocks5Password": "Proxy Password", - "hostDefaultsMetrics": "Host Metrics", - "hostDefaultsMetricsEnabled": "Enable Metrics", - "hostDefaultsMetricsEnabledDesc": "Collect CPU, memory, and other stats on new hosts by default", "hostDefaultsStatusCheckEnabled": "Enable Status Check", "hostDefaultsStatusCheckEnabledDesc": "Poll online/offline status on new hosts by default", + "hostDefaultsStatus": "Status Checks", "hostDefaultsTerminal": "Terminal", "hostDefaultsAutoTmux": "Auto-Tmux", "hostDefaultsAutoTmuxDesc": "Run new hosts' terminals inside tmux so sessions survive disconnects (needs tmux on the host)", diff --git a/src/ui/locales/translated/af_ZA.json b/src/ui/locales/translated/af_ZA.json index e8a10303e..e120e9c95 100644 --- a/src/ui/locales/translated/af_ZA.json +++ b/src/ui/locales/translated/af_ZA.json @@ -268,7 +268,6 @@ "showDisk": "Wys skyfgebruik", "addToFolder": "Voeg widget by vouer", "noHostSelected": "Geen gasheer gekies nie", - "metricsNotAvailable": "Metrieke nie beskikbaar nie", "selectHost": "Kies 'n gasheer...", "displayedMetrics": "Vertoonde Metrieke", "metricCpu": "SVE", @@ -276,13 +275,8 @@ "metricDisk": "Skyf", "metricUptime": "Optyd", "metricSystem": "Stelsel", - "metricOs": "OS", - "metricKernel": "Kern", - "metricHostname": "Gasheernaam", "metricNetwork": "Netwerk", "metricProcesses": "Prosesse", - "metricProcessesTotal": "totaal", - "metricProcessesRunning": "hardloop", "categoryMonitoring": "Monitering", "loading": "Laai tans...", "allClear": "Alles duidelik", @@ -775,10 +769,6 @@ "discardChanges": "Gooi veranderinge weg", "movedToFolder": "Het {{count}} gasheer(s) na \"{{folder}} \" geskuif", "editHostTooltip": "Wysig gasheer", - "statusChecks": "Statuskontroles", - "metricsCollection": "Metrieke-versameling", - "metricsInterval": "Metrieke Versamelingsinterval", - "metricsIntervalDesc": "Hoe gereeld om bedienerstatistieke in te samel (5s - 1u)", "behavior": "Gedrag", "themePreview": "Temavoorskou", "theme": "Tema", @@ -1899,8 +1889,6 @@ "serverStats": "Bedienerstatistieke", "loadingServerStats": "Laai bedienerstatistieke...", "noServerData": "Geen bedienerdata beskikbaar nie", - "cpu": "SVE", - "ram": "RAM", "customizeLayout": "Pasmaak Dashboard", "dashboardSettings": "Dashboard-instellings", "enableDisableCards": "Aktiveer/Deaktiveer Kaarte", @@ -1952,7 +1940,6 @@ "serviceLinksUrlPlaceholder": "http://192.168.1.10:8080", "serviceLinksInvalidUrl": "Voer 'n geldige webadres in", "serviceLinksAddFailed": "Kon nie diensskakel byvoeg nie", - "disk": "Skyf", "viewServerDetails": "Bekyk bedienerbesonderhede" }, "admin": { @@ -2091,7 +2078,6 @@ "sessionTimeoutRange": "Min 1 uur · Maks 720 uur", "monitoringDefaults": "Monitering van Standaardwaardes", "statusCheck": "Statuskontrole", - "metrics": "Metrieke", "sec": "sek", "logLevel": "Logvlak", "enableGuacamole": "Aktiveer Guacamole", @@ -2353,9 +2339,6 @@ "hostDefaultsSocks5Host": "Proxy-gasheer / -poort", "hostDefaultsSocks5Username": "Proxy-gebruikersnaam", "hostDefaultsSocks5Password": "Proxy-wagwoord", - "hostDefaultsMetrics": "Gasheermetrieke", - "hostDefaultsMetricsEnabled": "Aktiveer Metrieke", - "hostDefaultsMetricsEnabledDesc": "Versamel standaard SVE-, geheue- en ander statistieke op nuwe gashere", "hostDefaultsStatusCheckEnabled": "Aktiveer Statuskontrole", "hostDefaultsStatusCheckEnabledDesc": "Stem aanlyn/aflyn status op nuwe gashere by verstek", "hostDefaultsTerminal": "Terminaal", diff --git a/src/ui/locales/translated/ar_SA.json b/src/ui/locales/translated/ar_SA.json index f25c2ec6f..ba5ca4260 100644 --- a/src/ui/locales/translated/ar_SA.json +++ b/src/ui/locales/translated/ar_SA.json @@ -268,7 +268,6 @@ "showDisk": "إظهار استخدام القرص", "addToFolder": "إضافة أداة إلى المجلد", "noHostSelected": "لم يتم تحديد أي مضيف", - "metricsNotAvailable": "المقاييس غير متوفرة", "selectHost": "اختيار مضيف...", "displayedMetrics": "المقاييس المعروضة", "metricCpu": "CPU", @@ -276,13 +275,8 @@ "metricDisk": "القرص", "metricUptime": "وقت التشغيل", "metricSystem": "النظام", - "metricOs": "نظام التشغيل", - "metricKernel": "النواة", - "metricHostname": "اسم المضيف", "metricNetwork": "الشبكة", "metricProcesses": "العمليات", - "metricProcessesTotal": "إجمالي", - "metricProcessesRunning": "قيد التشغيل", "categoryMonitoring": "المراقبة", "loading": "جار التحميل...", "allClear": "كل شيء طبيعي", @@ -775,10 +769,6 @@ "discardChanges": "تجاهل التغييرات", "movedToFolder": "تم نقل {{count}} مضيف إلى \"{{folder}}\"", "editHostTooltip": "تعديل المضيف", - "statusChecks": "فحوصات الحالة", - "metricsCollection": "جمع المقاييس", - "metricsInterval": "فترة جمع المقاييس", - "metricsIntervalDesc": "مدة جمع إحصائيات الخادم (5s - 1h)", "behavior": "السلوك", "themePreview": "معاينة السمة", "theme": "السمة", @@ -1899,8 +1889,6 @@ "serverStats": "إحصائيات الخادم", "loadingServerStats": "جارٍ تحميل إحصائيات الخادم...", "noServerData": "لا تتوفر بيانات الخادم", - "cpu": "المعالج", - "ram": "الذاكرة", "customizeLayout": "تخصيص لوحة التحكم", "dashboardSettings": "إعدادات لوحة التحكم", "enableDisableCards": "تمكين/تعطيل البطاقات", @@ -1952,7 +1940,6 @@ "serviceLinksUrlPlaceholder": "http://192.168.1.10:8080", "serviceLinksInvalidUrl": "أدخل عنوان ويب صالح", "serviceLinksAddFailed": "فشل في إضافة رابط الخدمة", - "disk": "القرص", "viewServerDetails": "عرض تفاصيل الخادم" }, "admin": { @@ -2091,7 +2078,6 @@ "sessionTimeoutRange": "الحد الأدنى 1 ساعة · الحد الأقصى 720 ساعة", "monitoringDefaults": "إعدادات المراقبة الافتراضية", "statusCheck": "فحص الحالة", - "metrics": "مقاييس", "sec": "ثانية", "logLevel": "مستوى السجل", "enableGuacamole": "تمكين Guacamole", @@ -2353,9 +2339,6 @@ "hostDefaultsSocks5Host": "مضيف الوكيل / المنفذ", "hostDefaultsSocks5Username": "اسم مستخدم الوكيل", "hostDefaultsSocks5Password": "كلمة مرور الوكيل", - "hostDefaultsMetrics": "مقاييس المضيف", - "hostDefaultsMetricsEnabled": "تمكين المقاييس", - "hostDefaultsMetricsEnabledDesc": "جمع إحصائيات المعالج والذاكرة وغيرها على المضيفين الجدد افتراضيًا", "hostDefaultsStatusCheckEnabled": "تمكين فحص الحالة", "hostDefaultsStatusCheckEnabledDesc": "استطلاع حالة الاتصال أو الانفصال على المضيفين الجدد افتراضيًا", "hostDefaultsTerminal": "الطرفية", diff --git a/src/ui/locales/translated/bg_BG.json b/src/ui/locales/translated/bg_BG.json index 64639e15b..5a183b1ff 100644 --- a/src/ui/locales/translated/bg_BG.json +++ b/src/ui/locales/translated/bg_BG.json @@ -268,7 +268,6 @@ "showDisk": "Показване на използването на диска", "addToFolder": "Добавяне на джаджа към папката", "noHostSelected": "Няма избран хост", - "metricsNotAvailable": "Показателите не са налични", "selectHost": "Изберете хост...", "displayedMetrics": "Показани показатели", "metricCpu": "Процесор", @@ -276,13 +275,8 @@ "metricDisk": "Диск", "metricUptime": "Време на работа", "metricSystem": "Система", - "metricOs": "ОС", - "metricKernel": "Ядро", - "metricHostname": "Име на хост", "metricNetwork": "Мрежа", "metricProcesses": "Процеси", - "metricProcessesTotal": "общо", - "metricProcessesRunning": "бягане", "categoryMonitoring": "Мониторинг", "loading": "Зареждане...", "allClear": "Всичко е чисто", @@ -775,10 +769,6 @@ "discardChanges": "Отхвърляне на промените", "movedToFolder": "Преместих {{count}} хост(ове) в „{{folder}}“", "editHostTooltip": "Редактиране на хоста", - "statusChecks": "Проверки на състоянието", - "metricsCollection": "Колекция от показатели", - "metricsInterval": "Интервал на събиране на показатели", - "metricsIntervalDesc": "Колко често да се събира статистика за сървъра (5 секунди - 1 час)", "behavior": "Поведение", "themePreview": "Преглед на темата", "theme": "Тема", @@ -1899,8 +1889,6 @@ "serverStats": "Статистика на сървъра", "loadingServerStats": "Зареждане на статистиката на сървъра...", "noServerData": "Няма налични данни от сървъра", - "cpu": "Процесор", - "ram": "RAM памет", "customizeLayout": "Персонализиране на таблото за управление", "dashboardSettings": "Настройки на таблото за управление", "enableDisableCards": "Активиране/деактивиране на карти", @@ -1952,7 +1940,6 @@ "serviceLinksUrlPlaceholder": "http://192.168.1.10:8080", "serviceLinksInvalidUrl": "Въведете валиден уеб адрес", "serviceLinksAddFailed": "Добавянето на линк към услугата не бе успешно", - "disk": "Диск", "viewServerDetails": "Преглед на подробности за сървъра" }, "admin": { @@ -2091,7 +2078,6 @@ "sessionTimeoutRange": "Мин. 1 ч. · Макс. 720 ч.", "monitoringDefaults": "Мониторинг на настройки по подразбиране", "statusCheck": "Проверка на състоянието", - "metrics": "Метрики", "sec": "сек", "logLevel": "Ниво на лога", "enableGuacamole": "Активиране на гуакамоле", @@ -2353,9 +2339,6 @@ "hostDefaultsSocks5Host": "Прокси хост / порт", "hostDefaultsSocks5Username": "Потребителско име на прокси", "hostDefaultsSocks5Password": "Парола за прокси сървър", - "hostDefaultsMetrics": "Метрики на хоста", - "hostDefaultsMetricsEnabled": "Активиране на показатели", - "hostDefaultsMetricsEnabledDesc": "Събиране на CPU, памет и други статистически данни за нови хостове по подразбиране", "hostDefaultsStatusCheckEnabled": "Активиране на проверка на състоянието", "hostDefaultsStatusCheckEnabledDesc": "По подразбиране се задава онлайн/офлайн статус на анкетата за нови хостове", "hostDefaultsTerminal": "Терминал", diff --git a/src/ui/locales/translated/bn_BD.json b/src/ui/locales/translated/bn_BD.json index 13d733d04..66a6a08b9 100644 --- a/src/ui/locales/translated/bn_BD.json +++ b/src/ui/locales/translated/bn_BD.json @@ -268,7 +268,6 @@ "showDisk": "ডিস্ক ব্যবহার দেখান", "addToFolder": "ফোল্ডারে উইজেট যোগ করুন", "noHostSelected": "কোন হোস্ট নির্বাচন করা হয়নি", - "metricsNotAvailable": "মেট্রিক্স উপলব্ধ নেই", "selectHost": "একজন হোস্ট নির্বাচন করুন...", "displayedMetrics": "প্রদর্শিত মেট্রিক্স", "metricCpu": "সিপিইউ", @@ -276,13 +275,8 @@ "metricDisk": "ডিস্ক", "metricUptime": "আপটাইম", "metricSystem": "সিস্টেম", - "metricOs": "ওএস", - "metricKernel": "কার্নেল", - "metricHostname": "হোস্টনাম", "metricNetwork": "নেটওয়ার্ক", "metricProcesses": "প্রক্রিয়া", - "metricProcessesTotal": "মোট", - "metricProcessesRunning": "দৌড়ানো", "categoryMonitoring": "পর্যবেক্ষণ", "loading": "লোড হচ্ছে...", "allClear": "সব পরিষ্কার", @@ -775,10 +769,6 @@ "discardChanges": "পরিবর্তনগুলি বাতিল করুন", "movedToFolder": "{{count}} হোস্ট(গুলি) \"{{folder}} \" এ সরানো হয়েছে", "editHostTooltip": "হোস্ট সম্পাদনা করুন", - "statusChecks": "অবস্থা যাচাই", - "metricsCollection": "মেট্রিক্স সংগ্রহ", - "metricsInterval": "মেট্রিক্স সংগ্রহের ব্যবধান", - "metricsIntervalDesc": "কত ঘন ঘন সার্ভার পরিসংখ্যান সংগ্রহ করতে হবে (৫ সেকেন্ড - ১ ঘণ্টা)", "behavior": "আচরণ", "themePreview": "থিম প্রিভিউ", "theme": "থিম", @@ -1899,8 +1889,6 @@ "serverStats": "সার্ভার পরিসংখ্যান", "loadingServerStats": "সার্ভারের পরিসংখ্যান লোড হচ্ছে...", "noServerData": "কোন সার্ভার ডেটা উপলব্ধ নেই", - "cpu": "সিপিইউ", - "ram": "রাম", "customizeLayout": "ড্যাশবোর্ড কাস্টমাইজ করুন", "dashboardSettings": "ড্যাশবোর্ড সেটিংস", "enableDisableCards": "কার্ডগুলি সক্রিয়/নিষ্ক্রিয় করুন", @@ -1952,7 +1940,6 @@ "serviceLinksUrlPlaceholder": "http://192.168.1.10:8080", "serviceLinksInvalidUrl": "একটি বৈধ ওয়েব ঠিকানা লিখুন", "serviceLinksAddFailed": "পরিষেবা লিঙ্ক যোগ করতে ব্যর্থ হয়েছে", - "disk": "ডিস্ক", "viewServerDetails": "সার্ভারের বিবরণ দেখুন" }, "admin": { @@ -2091,7 +2078,6 @@ "sessionTimeoutRange": "সর্বনিম্ন ১ ঘণ্টা · সর্বোচ্চ ৭২০ ঘণ্টা", "monitoringDefaults": "ডিফল্ট নিরীক্ষণ", "statusCheck": "অবস্থা যাচাই", - "metrics": "মেট্রিক্স", "sec": "সেকেন্ড", "logLevel": "লগ স্তর", "enableGuacamole": "গুয়াকামোল সক্রিয় করুন", @@ -2353,9 +2339,6 @@ "hostDefaultsSocks5Host": "প্রক্সি হোস্ট / পোর্ট", "hostDefaultsSocks5Username": "প্রক্সি ব্যবহারকারীর নাম", "hostDefaultsSocks5Password": "প্রক্সি পাসওয়ার্ড", - "hostDefaultsMetrics": "হোস্ট মেট্রিক্স", - "hostDefaultsMetricsEnabled": "মেট্রিক্স সক্ষম করুন", - "hostDefaultsMetricsEnabledDesc": "ডিফল্টরূপে নতুন হোস্টগুলিতে সিপিইউ, মেমরি এবং অন্যান্য পরিসংখ্যান সংগ্রহ করুন।", "hostDefaultsStatusCheckEnabled": "স্ট্যাটাস চেক সক্ষম করুন", "hostDefaultsStatusCheckEnabledDesc": "নতুন হোস্টগুলিতে ডিফল্টরূপে অনলাইন/অফলাইন স্ট্যাটাস পোল করুন।", "hostDefaultsTerminal": "টার্মিনাল", diff --git a/src/ui/locales/translated/ca_ES.json b/src/ui/locales/translated/ca_ES.json index 9a984b4e8..9984a5505 100644 --- a/src/ui/locales/translated/ca_ES.json +++ b/src/ui/locales/translated/ca_ES.json @@ -268,7 +268,6 @@ "showDisk": "Mostra l'ús del disc", "addToFolder": "Afegeix el widget a la carpeta", "noHostSelected": "No s'ha seleccionat cap amfitrió", - "metricsNotAvailable": "Mètriques no disponibles", "selectHost": "Seleccioneu un amfitrió...", "displayedMetrics": "Mètriques mostrades", "metricCpu": "CPU", @@ -276,13 +275,8 @@ "metricDisk": "Disc", "metricUptime": "Temps de funcionament", "metricSystem": "Sistema", - "metricOs": "Sistema operatiu", - "metricKernel": "Nucli", - "metricHostname": "Nom d'amfitrió", "metricNetwork": "Xarxa", "metricProcesses": "Processos", - "metricProcessesTotal": "total", - "metricProcessesRunning": "corrent", "categoryMonitoring": "Monitorització", "loading": "S'està carregant...", "allClear": "Tot clar", @@ -775,10 +769,6 @@ "discardChanges": "Descarta els canvis", "movedToFolder": "S'han mogut {{count}} amfitrions a \"{{folder}}\"", "editHostTooltip": "Edita l'amfitrió", - "statusChecks": "Comprovacions d'estat", - "metricsCollection": "Col·lecció de mètriques", - "metricsInterval": "Interval de recopilació de mètriques", - "metricsIntervalDesc": "Amb quina freqüència s'han de recopilar estadístiques del servidor (5 segons - 1 hora)", "behavior": "Comportament", "themePreview": "Vista prèvia del tema", "theme": "Tema", @@ -1899,8 +1889,6 @@ "serverStats": "Estadístiques del servidor", "loadingServerStats": "S'estan carregant les estadístiques del servidor...", "noServerData": "No hi ha dades del servidor disponibles", - "cpu": "CPU", - "ram": "RAM", "customizeLayout": "Personalitza el tauler de control", "dashboardSettings": "Configuració del tauler de control", "enableDisableCards": "Activa/Desactiva les targetes", @@ -1952,7 +1940,6 @@ "serviceLinksUrlPlaceholder": "http://192.168.1.10:8080", "serviceLinksInvalidUrl": "Introduïu una adreça web vàlida", "serviceLinksAddFailed": "No s'ha pogut afegir l'enllaç del servei", - "disk": "Disc", "viewServerDetails": "Veure els detalls del servidor" }, "admin": { @@ -2091,7 +2078,6 @@ "sessionTimeoutRange": "Mín 1 h · Màx 720 h", "monitoringDefaults": "Monitorització dels valors predeterminats", "statusCheck": "Comprovació d'estat", - "metrics": "Mètriques", "sec": "segons", "logLevel": "Nivell de registre", "enableGuacamole": "Activa el guacamole", @@ -2353,9 +2339,6 @@ "hostDefaultsSocks5Host": "Amfitrió/port del proxy", "hostDefaultsSocks5Username": "Nom d'usuari del proxy", "hostDefaultsSocks5Password": "Contrasenya del servidor intermediari", - "hostDefaultsMetrics": "Mètriques de l'amfitrió", - "hostDefaultsMetricsEnabled": "Activa les mètriques", - "hostDefaultsMetricsEnabledDesc": "Recopila la CPU, la memòria i altres estadístiques dels nous amfitrions per defecte", "hostDefaultsStatusCheckEnabled": "Activa la comprovació d'estat", "hostDefaultsStatusCheckEnabledDesc": "Sondeja l'estat en línia/fora de línia dels nous amfitrions per defecte", "hostDefaultsTerminal": "Terminal", diff --git a/src/ui/locales/translated/cs_CZ.json b/src/ui/locales/translated/cs_CZ.json index 8d10e2b05..76ce3ca12 100644 --- a/src/ui/locales/translated/cs_CZ.json +++ b/src/ui/locales/translated/cs_CZ.json @@ -268,7 +268,6 @@ "showDisk": "Zobrazit využití disku", "addToFolder": "Přidat widget do složky", "noHostSelected": "Nebyl vybrán žádný hostitel", - "metricsNotAvailable": "Metriky nejsou k dispozici", "selectHost": "Vyberte hostitele...", "displayedMetrics": "Zobrazené metriky", "metricCpu": "Procesor", @@ -276,13 +275,8 @@ "metricDisk": "Disk", "metricUptime": "Doba provozuschopnosti", "metricSystem": "Systém", - "metricOs": "OS", - "metricKernel": "Jádro", - "metricHostname": "Název hostitele", "metricNetwork": "Síť", "metricProcesses": "Procesy", - "metricProcessesTotal": "celkový", - "metricProcessesRunning": "běh", "categoryMonitoring": "Monitorování", "loading": "Načítání...", "allClear": "Všechno čisté", @@ -775,10 +769,6 @@ "discardChanges": "Zahodit změny", "movedToFolder": "Přesunuto {{count}} hostitelů do „{{folder}}“", "editHostTooltip": "Upravit hostitele", - "statusChecks": "Kontroly stavu", - "metricsCollection": "Sbírka metrik", - "metricsInterval": "Interval sběru metrik", - "metricsIntervalDesc": "Jak často shromažďovat statistiky serveru (5 s - 1 h)", "behavior": "Chování", "themePreview": "Náhled motivu", "theme": "Téma", @@ -1899,8 +1889,6 @@ "serverStats": "Statistiky serveru", "loadingServerStats": "Načítání statistik serveru...", "noServerData": "Nejsou k dispozici žádná data serveru", - "cpu": "Procesor", - "ram": "BERAN", "customizeLayout": "Přizpůsobit řídicí panel", "dashboardSettings": "Nastavení řídicího panelu", "enableDisableCards": "Povolit/Zakázat karty", @@ -1952,7 +1940,6 @@ "serviceLinksUrlPlaceholder": "http://192.168.1.10:8080", "serviceLinksInvalidUrl": "Zadejte platnou webovou adresu", "serviceLinksAddFailed": "Nepodařilo se přidat odkaz na službu", - "disk": "Disk", "viewServerDetails": "Zobrazit podrobnosti o serveru" }, "admin": { @@ -2091,7 +2078,6 @@ "sessionTimeoutRange": "Min. 1h · Max. 720h", "monitoringDefaults": "Výchozí nastavení monitorování", "statusCheck": "Kontrola stavu", - "metrics": "Metriky", "sec": "sekunda", "logLevel": "Úroveň protokolu", "enableGuacamole": "Povolit guacamole", @@ -2353,9 +2339,6 @@ "hostDefaultsSocks5Host": "Proxy hostitel / port", "hostDefaultsSocks5Username": "Uživatelské jméno proxy serveru", "hostDefaultsSocks5Password": "Heslo proxy serveru", - "hostDefaultsMetrics": "Metriky hostitele", - "hostDefaultsMetricsEnabled": "Povolit metriky", - "hostDefaultsMetricsEnabledDesc": "Ve výchozím nastavení shromažďovat statistiky CPU, paměti a další statistiky na nových hostitelích", "hostDefaultsStatusCheckEnabled": "Povolit kontrolu stavu", "hostDefaultsStatusCheckEnabledDesc": "Ve výchozím nastavení zobrazovat stav online/offline na nových hostitelích", "hostDefaultsTerminal": "Terminál", diff --git a/src/ui/locales/translated/da_DK.json b/src/ui/locales/translated/da_DK.json index 929eb6adc..5d3a4ac39 100644 --- a/src/ui/locales/translated/da_DK.json +++ b/src/ui/locales/translated/da_DK.json @@ -268,7 +268,6 @@ "showDisk": "Vis diskforbrug", "addToFolder": "Tilføj widget til mappe", "noHostSelected": "Ingen vært valgt", - "metricsNotAvailable": "Målinger er ikke tilgængelige", "selectHost": "Vælg en vært...", "displayedMetrics": "Viste målinger", "metricCpu": "CPU", @@ -276,13 +275,8 @@ "metricDisk": "Disk", "metricUptime": "Oppetid", "metricSystem": "System", - "metricOs": "OS", - "metricKernel": "Kernen", - "metricHostname": "Værtsnavn", "metricNetwork": "Netværk", "metricProcesses": "Processer", - "metricProcessesTotal": "total", - "metricProcessesRunning": "løb", "categoryMonitoring": "Overvågning", "loading": "Indlæser...", "allClear": "Alt klart", @@ -775,10 +769,6 @@ "discardChanges": "Kassér ændringer", "movedToFolder": "Flyttede {{count}} vært(er) til \"{{folder}}\"", "editHostTooltip": "Rediger vært", - "statusChecks": "Statuskontroller", - "metricsCollection": "Metriksamling", - "metricsInterval": "Interval for indsamling af metrikker", - "metricsIntervalDesc": "Hvor ofte skal serverstatistik indsamles (5 sekunder - 1 time)", "behavior": "Opførsel", "themePreview": "Temaforhåndsvisning", "theme": "Tema", @@ -1899,8 +1889,6 @@ "serverStats": "Serverstatistik", "loadingServerStats": "Indlæser serverstatistik...", "noServerData": "Ingen serverdata tilgængelige", - "cpu": "CPU", - "ram": "VÆDDER", "customizeLayout": "Tilpas dashboard", "dashboardSettings": "Dashboardindstillinger", "enableDisableCards": "Aktivér/deaktiver kort", @@ -1952,7 +1940,6 @@ "serviceLinksUrlPlaceholder": "http://192.168.1.10:8080", "serviceLinksInvalidUrl": "Indtast en gyldig webadresse", "serviceLinksAddFailed": "Kunne ikke tilføje tjenestelink", - "disk": "Disk", "viewServerDetails": "Se serveroplysninger" }, "admin": { @@ -2091,7 +2078,6 @@ "sessionTimeoutRange": "Min. 1 time · Maks. 720 timer", "monitoringDefaults": "Overvågning af standardindstillinger", "statusCheck": "Statuskontrol", - "metrics": "Målinger", "sec": "sek.", "logLevel": "Logniveau", "enableGuacamole": "Aktivér guacamole", @@ -2353,9 +2339,6 @@ "hostDefaultsSocks5Host": "Proxy-vært / -port", "hostDefaultsSocks5Username": "Proxy-brugernavn", "hostDefaultsSocks5Password": "Proxy-adgangskode", - "hostDefaultsMetrics": "Værtsmålinger", - "hostDefaultsMetricsEnabled": "Aktivér metrikker", - "hostDefaultsMetricsEnabledDesc": "Indsaml som standard CPU-, hukommelses- og andre statistikker på nye værter", "hostDefaultsStatusCheckEnabled": "Aktivér statuskontrol", "hostDefaultsStatusCheckEnabledDesc": "Afstem online/offline status på nye værter som standard", "hostDefaultsTerminal": "Terminal", diff --git a/src/ui/locales/translated/de_DE.json b/src/ui/locales/translated/de_DE.json index eadb20ba1..d3ade0bc0 100644 --- a/src/ui/locales/translated/de_DE.json +++ b/src/ui/locales/translated/de_DE.json @@ -268,7 +268,6 @@ "showDisk": "Festplattennutzung anzeigen", "addToFolder": "Widget zum Ordner hinzufügen", "noHostSelected": "Kein Host ausgewählt", - "metricsNotAvailable": "Metriken nicht verfügbar", "selectHost": "Host auswählen...", "displayedMetrics": "Angezeigte Metriken", "metricCpu": "CPU", @@ -276,13 +275,8 @@ "metricDisk": "Festplatte", "metricUptime": "Betriebszeit", "metricSystem": "System", - "metricOs": "Betriebssystem", - "metricKernel": "Kernel", - "metricHostname": "Hostname", "metricNetwork": "Netzwerk", "metricProcesses": "Prozesse", - "metricProcessesTotal": "gesamt", - "metricProcessesRunning": "laufend", "categoryMonitoring": "Überwachung", "loading": "Wird geladen...", "allClear": "Alles in Ordnung", @@ -775,10 +769,6 @@ "discardChanges": "Änderungen verwerfen", "movedToFolder": "{{count}} Host(s) nach „{{folder}}“ verschoben", "editHostTooltip": "Host bearbeiten", - "statusChecks": "Statusprüfungen", - "metricsCollection": "Metrikerfassung", - "metricsInterval": "Metrikerfassungsintervall", - "metricsIntervalDesc": "Wie oft Serverstatistiken erfasst werden sollen (5s - 1h)", "behavior": "Verhalten", "themePreview": "Designvorschau", "theme": "Design", @@ -1899,8 +1889,6 @@ "serverStats": "Server-Statistiken", "loadingServerStats": "Server-Statistiken werden geladen...", "noServerData": "Keine Serverdaten verfügbar", - "cpu": "CPU", - "ram": "RAM", "customizeLayout": "Dashboard anpassen", "dashboardSettings": "Dashboard-Einstellungen", "enableDisableCards": "Karten aktivieren/deaktivieren", @@ -1952,7 +1940,6 @@ "serviceLinksUrlPlaceholder": "http://192.168.1.10:8080", "serviceLinksInvalidUrl": "Geben Sie eine gültige Webadresse ein", "serviceLinksAddFailed": "Hinzufügen des Service-Links fehlgeschlagen", - "disk": "Datenträger", "viewServerDetails": "Serverdetails anzeigen" }, "admin": { @@ -2091,7 +2078,6 @@ "sessionTimeoutRange": "Min. 1 Std. · Max. 720 Std.", "monitoringDefaults": "Überwachungsvoreinstellungen", "statusCheck": "Statusprüfung", - "metrics": "Metriken", "sec": "Sek.", "logLevel": "Log-Level", "enableGuacamole": "Guacamole aktivieren", @@ -2353,9 +2339,6 @@ "hostDefaultsSocks5Host": "Proxy-Host / Port", "hostDefaultsSocks5Username": "Proxy-Benutzername", "hostDefaultsSocks5Password": "Proxy-Passwort", - "hostDefaultsMetrics": "Host-Metriken", - "hostDefaultsMetricsEnabled": "Metriken aktivieren", - "hostDefaultsMetricsEnabledDesc": "CPU, Arbeitsspeicher und andere Statistiken standardmäßig auf neuen Hosts erfassen", "hostDefaultsStatusCheckEnabled": "Statusüberprüfung aktivieren", "hostDefaultsStatusCheckEnabledDesc": "Online-/Offline-Status standardmäßig auf neuen Hosts abfragen", "hostDefaultsTerminal": "Terminal", diff --git a/src/ui/locales/translated/el_GR.json b/src/ui/locales/translated/el_GR.json index f94a23f12..1cfb65a98 100644 --- a/src/ui/locales/translated/el_GR.json +++ b/src/ui/locales/translated/el_GR.json @@ -268,7 +268,6 @@ "showDisk": "Εμφάνιση χρήσης δίσκου", "addToFolder": "Προσθήκη γραφικού στοιχείου σε φάκελο", "noHostSelected": "Δεν έχει επιλεγεί κεντρικός υπολογιστής", - "metricsNotAvailable": "Οι μετρήσεις δεν είναι διαθέσιμες", "selectHost": "Επιλέξτε έναν οικοδεσπότη...", "displayedMetrics": "Εμφανιζόμενες μετρήσεις", "metricCpu": "ΚΜΕ", @@ -276,13 +275,8 @@ "metricDisk": "Δίσκος", "metricUptime": "Χρόνος λειτουργίας", "metricSystem": "Σύστημα", - "metricOs": "Λειτουργικό σύστημα", - "metricKernel": "Πυρήνας", - "metricHostname": "Όνομα κεντρικού υπολογιστή", "metricNetwork": "Δίκτυο", "metricProcesses": "Διαδικασίες", - "metricProcessesTotal": "σύνολο", - "metricProcessesRunning": "τρέξιμο", "categoryMonitoring": "Παρακολούθηση", "loading": "Φόρτωση...", "allClear": "Ορίζοντας είναι σαφής", @@ -775,10 +769,6 @@ "discardChanges": "Απόρριψη αλλαγών", "movedToFolder": "Μετακινήθηκαν οι {{count}} κεντρικοί υπολογιστές σε \"{{folder}}\"", "editHostTooltip": "Επεξεργασία κεντρικού υπολογιστή", - "statusChecks": "Έλεγχοι κατάστασης", - "metricsCollection": "Συλλογή μετρήσεων", - "metricsInterval": "Διάστημα συλλογής μετρήσεων", - "metricsIntervalDesc": "Πόσο συχνά να συλλέγονται στατιστικά στοιχεία διακομιστή (5 δευτ. - 1 ώρα)", "behavior": "Συμπεριφορά", "themePreview": "Προεπισκόπηση θέματος", "theme": "Θέμα", @@ -1899,8 +1889,6 @@ "serverStats": "Στατιστικά διακομιστή", "loadingServerStats": "Φόρτωση στατιστικών διακομιστή...", "noServerData": "Δεν υπάρχουν διαθέσιμα δεδομένα διακομιστή", - "cpu": "ΚΜΕ", - "ram": "ΕΜΒΟΛΟ", "customizeLayout": "Προσαρμογή Πίνακα Ελέγχου", "dashboardSettings": "Ρυθμίσεις πίνακα ελέγχου", "enableDisableCards": "Ενεργοποίηση/Απενεργοποίηση καρτών", @@ -1952,7 +1940,6 @@ "serviceLinksUrlPlaceholder": "http://192.168.1.10:8080", "serviceLinksInvalidUrl": "Εισαγάγετε μια έγκυρη διεύθυνση ιστού", "serviceLinksAddFailed": "Αποτυχία προσθήκης συνδέσμου υπηρεσίας", - "disk": "Δίσκος", "viewServerDetails": "Προβολή λεπτομερειών διακομιστή" }, "admin": { @@ -2091,7 +2078,6 @@ "sessionTimeoutRange": "Ελάχ. 1 ώρα · Μέγ. 720 ώρες", "monitoringDefaults": "Παρακολούθηση προεπιλογών", "statusCheck": "Έλεγχος κατάστασης", - "metrics": "Μετρήσεις", "sec": "δευτ.", "logLevel": "Επίπεδο καταγραφής", "enableGuacamole": "Ενεργοποίηση Γουακαμόλε", @@ -2353,9 +2339,6 @@ "hostDefaultsSocks5Host": "Κεντρικός υπολογιστής / θύρα μεσολάβησης", "hostDefaultsSocks5Username": "Όνομα χρήστη διακομιστή μεσολάβησης", "hostDefaultsSocks5Password": "Κωδικός πρόσβασης διακομιστή μεσολάβησης", - "hostDefaultsMetrics": "Μετρήσεις κεντρικού υπολογιστή", - "hostDefaultsMetricsEnabled": "Ενεργοποίηση μετρήσεων", - "hostDefaultsMetricsEnabledDesc": "Συλλογή CPU, μνήμης και άλλων στατιστικών σε νέους κεντρικούς υπολογιστές από προεπιλογή", "hostDefaultsStatusCheckEnabled": "Ενεργοποίηση ελέγχου κατάστασης", "hostDefaultsStatusCheckEnabledDesc": "Δημοσκόπηση κατάστασης online/offline σε νέους κεντρικούς υπολογιστές από προεπιλογή", "hostDefaultsTerminal": "Τερματικό", diff --git a/src/ui/locales/translated/es_ES.json b/src/ui/locales/translated/es_ES.json index ca2364167..59186c380 100644 --- a/src/ui/locales/translated/es_ES.json +++ b/src/ui/locales/translated/es_ES.json @@ -268,7 +268,6 @@ "showDisk": "Mostrar uso de disco", "addToFolder": "Añadir widget a la carpeta", "noHostSelected": "Ningún host seleccionado", - "metricsNotAvailable": "Métricas no disponibles", "selectHost": "Seleccionar un host...", "displayedMetrics": "Métricas mostradas", "metricCpu": "CPU", @@ -276,13 +275,8 @@ "metricDisk": "Disco", "metricUptime": "Tiempo de actividad", "metricSystem": "Sistema", - "metricOs": "SO", - "metricKernel": "Kernel", - "metricHostname": "Nombre de host", "metricNetwork": "Red", "metricProcesses": "Procesos", - "metricProcessesTotal": "total", - "metricProcessesRunning": "en ejecución", "categoryMonitoring": "Monitorización", "loading": "Cargando...", "allClear": "Todo correcto", @@ -775,10 +769,6 @@ "discardChanges": "Descartar cambios", "movedToFolder": "Movido(s) {{count}} host(s) a \"{{folder}}\"", "editHostTooltip": "Editar host", - "statusChecks": "Comprobaciones de estado", - "metricsCollection": "Recopilación de métricas", - "metricsInterval": "Intervalo de recopilación de métricas", - "metricsIntervalDesc": "Frecuencia de recopilación de estadísticas del servidor (5s - 1h)", "behavior": "Comportamiento", "themePreview": "Vista previa del tema", "theme": "Tema", @@ -1899,8 +1889,6 @@ "serverStats": "Estadísticas del servidor", "loadingServerStats": "Cargando estadísticas del servidor...", "noServerData": "No hay datos del servidor disponibles", - "cpu": "CPU", - "ram": "RAM", "customizeLayout": "Personalizar panel", "dashboardSettings": "Configuración del panel", "enableDisableCards": "Activar/Desactivar tarjetas", @@ -1952,7 +1940,6 @@ "serviceLinksUrlPlaceholder": "http://192.168.1.10:8080", "serviceLinksInvalidUrl": "Introduce una dirección web válida", "serviceLinksAddFailed": "Error al añadir el enlace de servicio", - "disk": "Disco", "viewServerDetails": "Ver detalles del servidor" }, "admin": { @@ -2091,7 +2078,6 @@ "sessionTimeoutRange": "Mín 1h · Máx 720h", "monitoringDefaults": "Configuración predeterminada de monitorización", "statusCheck": "Comprobación de estado", - "metrics": "Métricas", "sec": "seg", "logLevel": "Nivel de registro", "enableGuacamole": "Activar Guacamole", @@ -2353,9 +2339,6 @@ "hostDefaultsSocks5Host": "Host / Puerto del proxy", "hostDefaultsSocks5Username": "Nombre de usuario del proxy", "hostDefaultsSocks5Password": "Contraseña del proxy", - "hostDefaultsMetrics": "Métricas del host", - "hostDefaultsMetricsEnabled": "Habilitar métricas", - "hostDefaultsMetricsEnabledDesc": "Recopilar CPU, memoria y otras estadísticas en hosts nuevos de forma predeterminada", "hostDefaultsStatusCheckEnabled": "Habilitar verificación de estado", "hostDefaultsStatusCheckEnabledDesc": "Consultar estado en línea/fuera de línea en hosts nuevos de forma predeterminada", "hostDefaultsTerminal": "Terminal", diff --git a/src/ui/locales/translated/fi_FI.json b/src/ui/locales/translated/fi_FI.json index 7d9b3f41d..2590203ab 100644 --- a/src/ui/locales/translated/fi_FI.json +++ b/src/ui/locales/translated/fi_FI.json @@ -268,7 +268,6 @@ "showDisk": "Näytä levyn käyttö", "addToFolder": "Lisää widget kansioon", "noHostSelected": "Ei valittua isäntää", - "metricsNotAvailable": "Mittareita ei ole saatavilla", "selectHost": "Valitse isäntä...", "displayedMetrics": "Näytetyt mittarit", "metricCpu": "Suoritin", @@ -276,13 +275,8 @@ "metricDisk": "Levy", "metricUptime": "Käyttöaika", "metricSystem": "Järjestelmä", - "metricOs": "Käyttöjärjestelmä", - "metricKernel": "Ydin", - "metricHostname": "Isäntänimi", "metricNetwork": "Verkko", "metricProcesses": "Prosessit", - "metricProcessesTotal": "kokonais-", - "metricProcessesRunning": "juokseminen", "categoryMonitoring": "Seuranta", "loading": "Ladataan...", "allClear": "Kaikki selvää", @@ -775,10 +769,6 @@ "discardChanges": "Hylkää muutokset", "movedToFolder": "Siirretty {{count}} isäntä(ä) kohteeseen \"{{folder}}\"", "editHostTooltip": "Muokkaa isäntää", - "statusChecks": "Tilatarkastukset", - "metricsCollection": "Mittarikokoelma", - "metricsInterval": "Mittaritietojen keräysväli", - "metricsIntervalDesc": "Palvelintilastojen keräämisen tiheys (5 s - 1 h)", "behavior": "Käyttäytyminen", "themePreview": "Teeman esikatselu", "theme": "Teema", @@ -1899,8 +1889,6 @@ "serverStats": "Palvelimen tilastot", "loadingServerStats": "Ladataan palvelimen tilastoja...", "noServerData": "Palvelintietoja ei saatavilla", - "cpu": "Suoritin", - "ram": "RAM-muistia", "customizeLayout": "Mukauta kojelautaa", "dashboardSettings": "Kojelaudan asetukset", "enableDisableCards": "Korttien käyttöönotto/pois käytöstä", @@ -1952,7 +1940,6 @@ "serviceLinksUrlPlaceholder": "http://192.168.1.10:8080", "serviceLinksInvalidUrl": "Anna kelvollinen verkko-osoite", "serviceLinksAddFailed": "Palvelulinkin lisääminen epäonnistui", - "disk": "Levy", "viewServerDetails": "Näytä palvelimen tiedot" }, "admin": { @@ -2091,7 +2078,6 @@ "sessionTimeoutRange": "Vähintään 1 h · Enintään 720 h", "monitoringDefaults": "Valvonnan oletusarvot", "statusCheck": "Tilatarkistus", - "metrics": "Mittarit", "sec": "sek", "logLevel": "Lokitaso", "enableGuacamole": "Ota guacamole käyttöön", @@ -2353,9 +2339,6 @@ "hostDefaultsSocks5Host": "Välityspalvelin/portti", "hostDefaultsSocks5Username": "Välityspalvelimen käyttäjätunnus", "hostDefaultsSocks5Password": "Välityspalvelimen salasana", - "hostDefaultsMetrics": "Isännän mittarit", - "hostDefaultsMetricsEnabled": "Ota mittarit käyttöön", - "hostDefaultsMetricsEnabledDesc": "Kerää oletuksena uusien isäntien suorittimen, muistin ja muita tilastoja", "hostDefaultsStatusCheckEnabled": "Ota tilan tarkistus käyttöön", "hostDefaultsStatusCheckEnabledDesc": "Kysely online/offline-tila uusilla isännöillä oletuksena", "hostDefaultsTerminal": "Terminaali", diff --git a/src/ui/locales/translated/fr_FR.json b/src/ui/locales/translated/fr_FR.json index 983d29379..64f1c265a 100644 --- a/src/ui/locales/translated/fr_FR.json +++ b/src/ui/locales/translated/fr_FR.json @@ -268,7 +268,6 @@ "showDisk": "Afficher l'utilisation du disque", "addToFolder": "Ajouter le widget au dossier", "noHostSelected": "Aucun hôte sélectionné", - "metricsNotAvailable": "Métriques non disponibles", "selectHost": "Sélectionner un hôte…", "displayedMetrics": "Métriques affichées", "metricCpu": "CPU", @@ -276,13 +275,8 @@ "metricDisk": "Disque", "metricUptime": "Uptime", "metricSystem": "Système", - "metricOs": "OS", - "metricKernel": "Noyau", - "metricHostname": "Nom d’hôte", "metricNetwork": "Réseau", "metricProcesses": "Processus", - "metricProcessesTotal": "total", - "metricProcessesRunning": "en cours", "categoryMonitoring": "Surveillance", "loading": "Chargement…", "allClear": "Aucune alerte", @@ -775,10 +769,6 @@ "discardChanges": "Rejeter les modifications", "movedToFolder": "{{count}} hôte(s) déplacé(s) vers \"{{folder}}\"", "editHostTooltip": "Modifier l'hôte", - "statusChecks": "Vérifications d'état", - "metricsCollection": "Collecte de métriques", - "metricsInterval": "Intervalle de collecte des métriques", - "metricsIntervalDesc": "Fréquence de collecte des statistiques du serveur (5 s - 1 h)", "behavior": "Comportement", "themePreview": "Aperçu du thème", "theme": "Thème", @@ -1899,8 +1889,6 @@ "serverStats": "Statistiques serveur", "loadingServerStats": "Chargement des statistiques serveur...", "noServerData": "Aucune donnée serveur disponible", - "cpu": "CPU", - "ram": "RAM", "customizeLayout": "Personnaliser le tableau de bord", "dashboardSettings": "Paramètres du tableau de bord", "enableDisableCards": "Activer/Désactiver les cartes", @@ -1952,7 +1940,6 @@ "serviceLinksUrlPlaceholder": "http://192.168.1.10:8080", "serviceLinksInvalidUrl": "Saisissez une adresse Web valide", "serviceLinksAddFailed": "Échec de l’ajout du lien de service", - "disk": "Disque", "viewServerDetails": "Afficher les détails du serveur" }, "admin": { @@ -2091,7 +2078,6 @@ "sessionTimeoutRange": "Min 1 h · Max 720 h", "monitoringDefaults": "Paramètres de surveillance par défaut", "statusCheck": "Vérification de l'état", - "metrics": "Métriques", "sec": "s", "logLevel": "Niveau de journalisation", "enableGuacamole": "Activer Guacamole", @@ -2353,9 +2339,6 @@ "hostDefaultsSocks5Host": "Hôte / Port du proxy", "hostDefaultsSocks5Username": "Nom d’utilisateur du proxy", "hostDefaultsSocks5Password": "Mot de passe du proxy", - "hostDefaultsMetrics": "Métriques de l’hôte", - "hostDefaultsMetricsEnabled": "Activer les métriques", - "hostDefaultsMetricsEnabledDesc": "Collecter par défaut les statistiques CPU, mémoire, etc. des nouveaux hôtes", "hostDefaultsStatusCheckEnabled": "Activer la vérification de l’état", "hostDefaultsStatusCheckEnabledDesc": "Sonder par défaut l’état en ligne/hors ligne des nouveaux hôtes", "hostDefaultsTerminal": "Terminal", diff --git a/src/ui/locales/translated/he_IL.json b/src/ui/locales/translated/he_IL.json index 08cfa3ba9..76836321d 100644 --- a/src/ui/locales/translated/he_IL.json +++ b/src/ui/locales/translated/he_IL.json @@ -268,7 +268,6 @@ "showDisk": "הצג שימוש בדיסק", "addToFolder": "הוסף ווידג'ט לתיקייה", "noHostSelected": "לא נבחר מארח", - "metricsNotAvailable": "מדדים אינם זמינים", "selectHost": "בחר מארח...", "displayedMetrics": "מדדים מוצגים", "metricCpu": "מעבד", @@ -276,13 +275,8 @@ "metricDisk": "דִיסק", "metricUptime": "זמן פעולה", "metricSystem": "מַעֲרֶכֶת", - "metricOs": "מערכת הפעלה", - "metricKernel": "גַרעִין", - "metricHostname": "שם מארח", "metricNetwork": "רֶשֶׁת", "metricProcesses": "תהליכים", - "metricProcessesTotal": "סַך הַכֹּל", - "metricProcessesRunning": "רִיצָה", "categoryMonitoring": "ניטור", "loading": "טְעִינָה...", "allClear": "הכל ברור", @@ -775,10 +769,6 @@ "discardChanges": "בטל שינויים", "movedToFolder": "הועבר/י מארח/ים {{count}} ל-\"{{folder}}\"", "editHostTooltip": "עריכת מארח", - "statusChecks": "בדיקות סטטוס", - "metricsCollection": "אוסף מדדים", - "metricsInterval": "מרווח איסוף מדדים", - "metricsIntervalDesc": "באיזו תדירות לאסוף סטטיסטיקות שרת (5 שניות - שעה)", "behavior": "הִתְנַהֲגוּת", "themePreview": "תצוגה מקדימה של ערכת נושא", "theme": "נוֹשֵׂא", @@ -1899,8 +1889,6 @@ "serverStats": "סטטיסטיקות שרת", "loadingServerStats": "טוען סטטיסטיקות שרת...", "noServerData": "אין נתוני שרת זמינים", - "cpu": "מעבד", - "ram": "אַיִל", "customizeLayout": "התאמה אישית של לוח המחוונים", "dashboardSettings": "הגדרות לוח המחוונים", "enableDisableCards": "הפעלה/השבתה של כרטיסים", @@ -1952,7 +1940,6 @@ "serviceLinksUrlPlaceholder": "http://192.168.1.10:8080", "serviceLinksInvalidUrl": "הזן כתובת אינטרנט חוקית", "serviceLinksAddFailed": "נכשל הוספת קישור לשירות", - "disk": "דִיסק", "viewServerDetails": "הצג פרטי שרת" }, "admin": { @@ -2091,7 +2078,6 @@ "sessionTimeoutRange": "מינימום שעה · מקסימום 720 שעות", "monitoringDefaults": "ניטור ברירות מחדל", "statusCheck": "בדיקת סטטוס", - "metrics": "מדדים", "sec": "שניות", "logLevel": "רמת יומן", "enableGuacamole": "הפעל גוואקמולי", @@ -2353,9 +2339,6 @@ "hostDefaultsSocks5Host": "מארח פרוקסי / פורט", "hostDefaultsSocks5Username": "שם משתמש פרוקסי", "hostDefaultsSocks5Password": "סיסמת פרוקסי", - "hostDefaultsMetrics": "מדדי מארח", - "hostDefaultsMetricsEnabled": "הפעל מדדים", - "hostDefaultsMetricsEnabledDesc": "איסוף נתונים סטטיסטיים של המעבד, הזיכרון ונתונים סטטיסטיים אחרים במחשבים חדשים כברירת מחדל", "hostDefaultsStatusCheckEnabled": "הפעל בדיקת סטטוס", "hostDefaultsStatusCheckEnabledDesc": "סקר סטטוס מקוון/לא מקוון על מארחים חדשים כברירת מחדל", "hostDefaultsTerminal": "מָסוֹף", diff --git a/src/ui/locales/translated/hi_IN.json b/src/ui/locales/translated/hi_IN.json index 0dae8523f..289aa7266 100644 --- a/src/ui/locales/translated/hi_IN.json +++ b/src/ui/locales/translated/hi_IN.json @@ -268,7 +268,6 @@ "showDisk": "डिस्क उपयोग दिखाएं", "addToFolder": "फ़ोल्डर में विजेट जोड़ें", "noHostSelected": "कोई होस्ट चयनित नहीं", - "metricsNotAvailable": "मेट्रिक्स उपलब्ध नहीं", "selectHost": "होस्ट चुनें...", "displayedMetrics": "प्रदर्शित मेट्रिक्स", "metricCpu": "CPU", @@ -276,13 +275,8 @@ "metricDisk": "डिस्क", "metricUptime": "अपटाइम", "metricSystem": "सिस्टम", - "metricOs": "ओएस", - "metricKernel": "कर्नेल", - "metricHostname": "होस्टनेम", "metricNetwork": "नेटवर्क", "metricProcesses": "प्रोसेस", - "metricProcessesTotal": "कुल", - "metricProcessesRunning": "चालू", "categoryMonitoring": "निगरानी", "loading": "लोड हो रहा है...", "allClear": "सब ठीक है", @@ -775,10 +769,6 @@ "discardChanges": "परिवर्तनों को निरस्त करें", "movedToFolder": "{{count}} होस्ट(s) को \"{{folder}}\" में स्थानांतरित किया गया", "editHostTooltip": "होस्ट संपादित करें", - "statusChecks": "स्थिति जांच", - "metricsCollection": "मेट्रिक्स संग्रहण", - "metricsInterval": "मेट्रिक्स संग्रहण अंतराल", - "metricsIntervalDesc": "सर्वर आँकड़े कितनी बार एकत्र करें (5s - 1h)", "behavior": "व्यवहार", "themePreview": "थीम पूर्वावलोकन", "theme": "थीम", @@ -1899,8 +1889,6 @@ "serverStats": "सर्वर आँकड़े", "loadingServerStats": "सर्वर आँकड़े लोड हो रहे हैं...", "noServerData": "कोई सर्वर डेटा उपलब्ध नहीं है", - "cpu": "CPU", - "ram": "RAM", "customizeLayout": "डैशबोर्ड अनुकूलित करें", "dashboardSettings": "डैशबोर्ड सेटिंग्स", "enableDisableCards": "कार्ड सक्षम/अक्षम करें", @@ -1952,7 +1940,6 @@ "serviceLinksUrlPlaceholder": "http://192.168.1.10:8080", "serviceLinksInvalidUrl": "एक मान्य वेब पता दर्ज करें", "serviceLinksAddFailed": "सर्विस लिंक जोड़ने में विफल", - "disk": "डिस्क", "viewServerDetails": "सर्वर विवरण देखें" }, "admin": { @@ -2091,7 +2078,6 @@ "sessionTimeoutRange": "न्यूनतम 1 घंटा · अधिकतम 720 घंटे", "monitoringDefaults": "निगरानी डिफ़ॉल्ट", "statusCheck": "स्थिति जाँच", - "metrics": "मेट्रिक्स", "sec": "सेकंड", "logLevel": "लॉग स्तर", "enableGuacamole": "Guacamole सक्षम करें", @@ -2353,9 +2339,6 @@ "hostDefaultsSocks5Host": "प्रॉक्सी होस्ट / पोर्ट", "hostDefaultsSocks5Username": "प्रॉक्सी उपयोगकर्ता नाम", "hostDefaultsSocks5Password": "प्रॉक्सी पासवर्ड", - "hostDefaultsMetrics": "होस्ट मेट्रिक्स", - "hostDefaultsMetricsEnabled": "मेट्रिक्स सक्षम करें", - "hostDefaultsMetricsEnabledDesc": "डिफ़ॉल्ट रूप से नए होस्ट पर CPU, मेमोरी और अन्य आँकड़े एकत्र करें", "hostDefaultsStatusCheckEnabled": "स्टेटस चेक सक्षम करें", "hostDefaultsStatusCheckEnabledDesc": "डिफ़ॉल्ट रूप से नए होस्ट पर ऑनलाइन/ऑफ़लाइन स्थिति की जाँच करें", "hostDefaultsTerminal": "टर्मिनल", diff --git a/src/ui/locales/translated/hu_HU.json b/src/ui/locales/translated/hu_HU.json index 6a4865d8d..3c03c2d2f 100644 --- a/src/ui/locales/translated/hu_HU.json +++ b/src/ui/locales/translated/hu_HU.json @@ -268,7 +268,6 @@ "showDisk": "Lemezhasználat megjelenítése", "addToFolder": "Widget hozzáadása mappához", "noHostSelected": "Nincs kiválasztva gazdagép", - "metricsNotAvailable": "Mutatók nem állnak rendelkezésre", "selectHost": "Válasszon egy hosztot...", "displayedMetrics": "Megjelenített mutatók", "metricCpu": "CPU", @@ -276,13 +275,8 @@ "metricDisk": "Korong", "metricUptime": "Üzemidő", "metricSystem": "Rendszer", - "metricOs": "operációs rendszer", - "metricKernel": "Kernel", - "metricHostname": "Gazdagépnév", "metricNetwork": "Hálózat", "metricProcesses": "Folyamatok", - "metricProcessesTotal": "teljes", - "metricProcessesRunning": "futás", "categoryMonitoring": "Monitoring", "loading": "Terhelés...", "allClear": "Minden tiszta", @@ -775,10 +769,6 @@ "discardChanges": "Változtatások elvetése", "movedToFolder": "{{count}} hoszt(oka)t áthelyeztem ide: \"{{folder}}\"", "editHostTooltip": "Gazdagép szerkesztése", - "statusChecks": "Állapotellenőrzések", - "metricsCollection": "Metrikagyűjtemény", - "metricsInterval": "Metrikagyűjtési intervallum", - "metricsIntervalDesc": "Milyen gyakran gyűjtsünk szerverstatisztikákat (5 másodperc - 1 óra)", "behavior": "Viselkedés", "themePreview": "Téma előnézete", "theme": "Téma", @@ -1899,8 +1889,6 @@ "serverStats": "Szerver statisztikák", "loadingServerStats": "Szerverstatisztikák betöltése...", "noServerData": "Nincsenek elérhető szerveradatok", - "cpu": "CPU", - "ram": "RAM", "customizeLayout": "Irányítópult testreszabása", "dashboardSettings": "Irányítópult beállításai", "enableDisableCards": "Kártyák engedélyezése/letiltása", @@ -1952,7 +1940,6 @@ "serviceLinksUrlPlaceholder": "http://192.168.1.10:8080", "serviceLinksInvalidUrl": "Adjon meg egy érvényes webcímet", "serviceLinksAddFailed": "Nem sikerült hozzáadni a szolgáltatás linkjét", - "disk": "Korong", "viewServerDetails": "Szerver részleteinek megtekintése" }, "admin": { @@ -2091,7 +2078,6 @@ "sessionTimeoutRange": "Minimum 1 óra · Max. 720 óra", "monitoringDefaults": "Monitorozási alapértelmezések", "statusCheck": "Állapotellenőrzés", - "metrics": "Metrikák", "sec": "másodperc", "logLevel": "Naplózási szint", "enableGuacamole": "Guacamole engedélyezése", @@ -2353,9 +2339,6 @@ "hostDefaultsSocks5Host": "Proxy host / port", "hostDefaultsSocks5Username": "Proxy felhasználónév", "hostDefaultsSocks5Password": "Proxy jelszó", - "hostDefaultsMetrics": "Gazdagép-metrikák", - "hostDefaultsMetricsEnabled": "Metrikák engedélyezése", - "hostDefaultsMetricsEnabledDesc": "Alapértelmezés szerint gyűjtsön CPU-, memória- és egyéb statisztikákat az új hosztokon", "hostDefaultsStatusCheckEnabled": "Állapotellenőrzés engedélyezése", "hostDefaultsStatusCheckEnabledDesc": "Online/offline állapot lekérdezése új gazdagépeken alapértelmezés szerint", "hostDefaultsTerminal": "Terminál", diff --git a/src/ui/locales/translated/id_ID.json b/src/ui/locales/translated/id_ID.json index 36cb1e9fc..7eabb70e6 100644 --- a/src/ui/locales/translated/id_ID.json +++ b/src/ui/locales/translated/id_ID.json @@ -268,7 +268,6 @@ "showDisk": "Tampilkan Penggunaan Disk", "addToFolder": "Tambahkan widget ke folder", "noHostSelected": "Tidak ada pembawa acara yang dipilih.", - "metricsNotAvailable": "Metrik tidak tersedia", "selectHost": "Pilih tuan rumah...", "displayedMetrics": "Metrik yang Ditampilkan", "metricCpu": "CPU", @@ -276,13 +275,8 @@ "metricDisk": "Disk", "metricUptime": "Waktu aktif", "metricSystem": "Sistem", - "metricOs": "Sistem Operasi", - "metricKernel": "Inti", - "metricHostname": "Nama host", "metricNetwork": "Jaringan", "metricProcesses": "Proses", - "metricProcessesTotal": "total", - "metricProcessesRunning": "berlari", "categoryMonitoring": "Pemantauan", "loading": "Memuat...", "allClear": "Semuanya aman.", @@ -775,10 +769,6 @@ "discardChanges": "Abaikan perubahan", "movedToFolder": "Host {{count}} dipindahkan ke \"{{folder}}\"", "editHostTooltip": "Edit host", - "statusChecks": "Pemeriksaan Status", - "metricsCollection": "Pengumpulan Metrik", - "metricsInterval": "Interval Pengumpulan Metrik", - "metricsIntervalDesc": "Seberapa sering mengumpulkan statistik server (5 detik - 1 jam)", "behavior": "Perilaku", "themePreview": "Pratinjau Tema", "theme": "Tema", @@ -1899,8 +1889,6 @@ "serverStats": "Statistik Server", "loadingServerStats": "Memuat statistik server...", "noServerData": "Tidak ada data server yang tersedia.", - "cpu": "CPU", - "ram": "RAM", "customizeLayout": "Sesuaikan Dasbor", "dashboardSettings": "Pengaturan Dasbor", "enableDisableCards": "Aktifkan/Nonaktifkan Kartu", @@ -1952,7 +1940,6 @@ "serviceLinksUrlPlaceholder": "http://192.168.1.10:8080", "serviceLinksInvalidUrl": "Masukkan alamat web yang valid", "serviceLinksAddFailed": "Gagal menambahkan tautan layanan", - "disk": "Disk", "viewServerDetails": "Lihat detail server" }, "admin": { @@ -2091,7 +2078,6 @@ "sessionTimeoutRange": "Minimal 1 jam · Maksimal 720 jam", "monitoringDefaults": "Pemantauan Default", "statusCheck": "Pemeriksaan Status", - "metrics": "Metrik", "sec": "detik", "logLevel": "Tingkat Log", "enableGuacamole": "Aktifkan Guacamole", @@ -2353,9 +2339,6 @@ "hostDefaultsSocks5Host": "Host/Port Proksi", "hostDefaultsSocks5Username": "Nama Pengguna Proksi", "hostDefaultsSocks5Password": "Kata Sandi Proksi", - "hostDefaultsMetrics": "Metrik Host", - "hostDefaultsMetricsEnabled": "Aktifkan Metrik", - "hostDefaultsMetricsEnabledDesc": "Secara default, kumpulkan statistik CPU, memori, dan statistik lainnya pada host baru.", "hostDefaultsStatusCheckEnabled": "Aktifkan Pemeriksaan Status", "hostDefaultsStatusCheckEnabledDesc": "Secara default, lakukan polling status online/offline pada host baru.", "hostDefaultsTerminal": "Terminal", diff --git a/src/ui/locales/translated/it_IT.json b/src/ui/locales/translated/it_IT.json index e5dfae5d2..32b301a52 100644 --- a/src/ui/locales/translated/it_IT.json +++ b/src/ui/locales/translated/it_IT.json @@ -268,7 +268,6 @@ "showDisk": "Mostra utilizzo disco", "addToFolder": "Aggiungi widget alla cartella", "noHostSelected": "Nessun host selezionato", - "metricsNotAvailable": "Metriche non disponibili", "selectHost": "Seleziona un host...", "displayedMetrics": "Metriche visualizzate", "metricCpu": "CPU", @@ -276,13 +275,8 @@ "metricDisk": "Disco", "metricUptime": "Uptime", "metricSystem": "Sistema", - "metricOs": "OS", - "metricKernel": "Kernel", - "metricHostname": "Nome host", "metricNetwork": "Rete", "metricProcesses": "Processi", - "metricProcessesTotal": "totale", - "metricProcessesRunning": "in esecuzione", "categoryMonitoring": "Monitoraggio", "loading": "Caricamento...", "allClear": "Tutto a posto", @@ -775,10 +769,6 @@ "discardChanges": "Scarta le modifiche", "movedToFolder": "Spostato/i {{count}} host nella cartella \"{{folder}}\"", "editHostTooltip": "Modifica host", - "statusChecks": "Controlli di stato", - "metricsCollection": "Raccolta metriche", - "metricsInterval": "Intervallo raccolta metriche", - "metricsIntervalDesc": "Intervallo di raccolta delle metriche del server (5s - 1h)", "behavior": "Comportamento", "themePreview": "Anteprima tema", "theme": "Tema", @@ -1899,8 +1889,6 @@ "serverStats": "Statistiche server", "loadingServerStats": "Caricamento statistiche server...", "noServerData": "Nessun dato del server disponibile", - "cpu": "CPU", - "ram": "RAM", "customizeLayout": "Personalizza Dashboard", "dashboardSettings": "Impostazioni Dashboard", "enableDisableCards": "Abilita/Disabilita schede", @@ -1952,7 +1940,6 @@ "serviceLinksUrlPlaceholder": "http://192.168.1.10:8080", "serviceLinksInvalidUrl": "Inserisci un indirizzo web valido", "serviceLinksAddFailed": "Impossibile aggiungere il collegamento al servizio", - "disk": "Disco", "viewServerDetails": "Visualizza dettagli server" }, "admin": { @@ -2091,7 +2078,6 @@ "sessionTimeoutRange": "Min 1h · Max 720h", "monitoringDefaults": "Impostazioni predefinite monitoraggio", "statusCheck": "Verifica stato", - "metrics": "Metriche", "sec": "sec", "logLevel": "Livello di log", "enableGuacamole": "Abilita Guacamole", @@ -2353,9 +2339,6 @@ "hostDefaultsSocks5Host": "Host / Porta proxy", "hostDefaultsSocks5Username": "Nome utente proxy", "hostDefaultsSocks5Password": "Password proxy", - "hostDefaultsMetrics": "Metriche host", - "hostDefaultsMetricsEnabled": "Abilita metriche", - "hostDefaultsMetricsEnabledDesc": "Raccogli metriche CPU, memoria e altre sui nuovi host per impostazione predefinita", "hostDefaultsStatusCheckEnabled": "Abilita controllo stato", "hostDefaultsStatusCheckEnabledDesc": "Verifica lo stato online/offline sui nuovi host per impostazione predefinita", "hostDefaultsTerminal": "Terminale", diff --git a/src/ui/locales/translated/ja_JP.json b/src/ui/locales/translated/ja_JP.json index 8cae3f976..bf9cc2ebc 100644 --- a/src/ui/locales/translated/ja_JP.json +++ b/src/ui/locales/translated/ja_JP.json @@ -268,7 +268,6 @@ "showDisk": "ディスク使用量を表示", "addToFolder": "ウィジェットをフォルダに追加", "noHostSelected": "ホストが選択されていません", - "metricsNotAvailable": "メトリクスは利用できません", "selectHost": "ホストを選択...", "displayedMetrics": "表示メトリクス", "metricCpu": "CPU", @@ -276,13 +275,8 @@ "metricDisk": "ディスク", "metricUptime": "稼働時間", "metricSystem": "システム", - "metricOs": "OS", - "metricKernel": "カーネル", - "metricHostname": "ホスト名", "metricNetwork": "ネットワーク", "metricProcesses": "プロセス", - "metricProcessesTotal": "合計", - "metricProcessesRunning": "実行中", "categoryMonitoring": "モニタリング", "loading": "読み込み中...", "allClear": "すべて正常", @@ -775,10 +769,6 @@ "discardChanges": "変更を破棄する", "movedToFolder": "{{count}} 個のホストを \"{{folder}}\" に移動しました", "editHostTooltip": "ホストを編集", - "statusChecks": "ステータスチェック", - "metricsCollection": "メトリクス収集", - "metricsInterval": "メトリクス収集間隔", - "metricsIntervalDesc": "サーバー統計を収集する頻度 (5秒〜1時間)", "behavior": "動作", "themePreview": "テーマプレビュー", "theme": "テーマ", @@ -1899,8 +1889,6 @@ "serverStats": "サーバー統計", "loadingServerStats": "サーバー統計を読み込み中...", "noServerData": "サーバーデータがありません", - "cpu": "CPU", - "ram": "RAM", "customizeLayout": "ダッシュボードのカスタマイズ", "dashboardSettings": "ダッシュボード設定", "enableDisableCards": "カードの表示/非表示", @@ -1952,7 +1940,6 @@ "serviceLinksUrlPlaceholder": "http://192.168.1.10:8080", "serviceLinksInvalidUrl": "有効なWebアドレスを入力してください", "serviceLinksAddFailed": "サービスリンクの追加に失敗しました", - "disk": "ディスク", "viewServerDetails": "サーバーの詳細を表示" }, "admin": { @@ -2091,7 +2078,6 @@ "sessionTimeoutRange": "最小1時間・最大720時間", "monitoringDefaults": "監視のデフォルト", "statusCheck": "ステータスチェック", - "metrics": "メトリクス", "sec": "秒", "logLevel": "ログレベル", "enableGuacamole": "Guacamoleを有効にする", @@ -2353,9 +2339,6 @@ "hostDefaultsSocks5Host": "プロキシホスト / ポート", "hostDefaultsSocks5Username": "プロキシユーザー名", "hostDefaultsSocks5Password": "プロキシパスワード", - "hostDefaultsMetrics": "ホストメトリクス", - "hostDefaultsMetricsEnabled": "メトリクスを有効にする", - "hostDefaultsMetricsEnabledDesc": "デフォルトで新規ホストのCPU、メモリなどの統計情報を収集する", "hostDefaultsStatusCheckEnabled": "ステータスチェックを有効にする", "hostDefaultsStatusCheckEnabledDesc": "デフォルトで新規ホストのオンライン/オフライン状態をポーリングする", "hostDefaultsTerminal": "ターミナル", diff --git a/src/ui/locales/translated/ko_KR.json b/src/ui/locales/translated/ko_KR.json index 589c076c6..876626860 100644 --- a/src/ui/locales/translated/ko_KR.json +++ b/src/ui/locales/translated/ko_KR.json @@ -268,7 +268,6 @@ "showDisk": "디스크 사용량 표시", "addToFolder": "폴더에 위젯 추가", "noHostSelected": "선택된 호스트 없음", - "metricsNotAvailable": "메트릭을 사용할 수 없습니다", "selectHost": "호스트 선택...", "displayedMetrics": "표시된 메트릭", "metricCpu": "CPU", @@ -276,13 +275,8 @@ "metricDisk": "디스크", "metricUptime": "업타임", "metricSystem": "시스템", - "metricOs": "OS", - "metricKernel": "커널", - "metricHostname": "호스트명", "metricNetwork": "네트워크", "metricProcesses": "프로세스", - "metricProcessesTotal": "전체", - "metricProcessesRunning": "실행 중", "categoryMonitoring": "모니터링", "loading": "로딩 중...", "allClear": "모두 정상", @@ -775,10 +769,6 @@ "discardChanges": "잔돈을 버리세요", "movedToFolder": "호스트 {{count}}개를 \"{{folder}}\" 폴더로 이동했습니다", "editHostTooltip": "호스트 편집", - "statusChecks": "상태 확인", - "metricsCollection": "메트릭 수집", - "metricsInterval": "메트릭 수집 간격", - "metricsIntervalDesc": "서버 통계 수집 빈도 (5초 - 1시간)", "behavior": "동작", "themePreview": "테마 미리보기", "theme": "테마", @@ -1899,8 +1889,6 @@ "serverStats": "서버 통계", "loadingServerStats": "서버 통계 로딩 중...", "noServerData": "서버 데이터 없음", - "cpu": "CPU", - "ram": "RAM", "customizeLayout": "대시보드 사용자 지정", "dashboardSettings": "대시보드 설정", "enableDisableCards": "카드 활성화/비활성화", @@ -1952,7 +1940,6 @@ "serviceLinksUrlPlaceholder": "http://192.168.1.10:8080", "serviceLinksInvalidUrl": "올바른 웹 주소를 입력하세요", "serviceLinksAddFailed": "서비스 링크 추가 실패", - "disk": "디스크", "viewServerDetails": "서버 세부정보 보기" }, "admin": { @@ -2091,7 +2078,6 @@ "sessionTimeoutRange": "최소 1시간 · 최대 720시간", "monitoringDefaults": "모니터링 기본값", "statusCheck": "상태 확인", - "metrics": "메트릭", "sec": "초", "logLevel": "로그 레벨", "enableGuacamole": "Guacamole 활성화", @@ -2353,9 +2339,6 @@ "hostDefaultsSocks5Host": "프록시 호스트 / 포트", "hostDefaultsSocks5Username": "프록시 사용자 이름", "hostDefaultsSocks5Password": "프록시 비밀번호", - "hostDefaultsMetrics": "호스트 메트릭", - "hostDefaultsMetricsEnabled": "메트릭 활성화", - "hostDefaultsMetricsEnabledDesc": "기본적으로 새 호스트에서 CPU, 메모리 등 통계 수집", "hostDefaultsStatusCheckEnabled": "상태 확인 활성화", "hostDefaultsStatusCheckEnabledDesc": "기본적으로 새 호스트의 온라인/오프라인 상태 확인", "hostDefaultsTerminal": "터미널", diff --git a/src/ui/locales/translated/nl_NL.json b/src/ui/locales/translated/nl_NL.json index 88f33341a..024ab31cc 100644 --- a/src/ui/locales/translated/nl_NL.json +++ b/src/ui/locales/translated/nl_NL.json @@ -268,7 +268,6 @@ "showDisk": "Schijfgebruik weergeven", "addToFolder": "Widget toevoegen aan map", "noHostSelected": "Geen host geselecteerd", - "metricsNotAvailable": "Gegevens niet beschikbaar", "selectHost": "Selecteer een host...", "displayedMetrics": "Weergegeven statistieken", "metricCpu": "CPU", @@ -276,13 +275,8 @@ "metricDisk": "Schijf", "metricUptime": "Beschikbaarheid", "metricSystem": "Systeem", - "metricOs": "OS", - "metricKernel": "Kernel", - "metricHostname": "Hostnaam", "metricNetwork": "Netwerk", "metricProcesses": "Processen", - "metricProcessesTotal": "totaal", - "metricProcessesRunning": "rennen", "categoryMonitoring": "Monitoring", "loading": "Laden...", "allClear": "Alles in orde", @@ -775,10 +769,6 @@ "discardChanges": "Wijzigingen negeren", "movedToFolder": "Verplaatst {{count}} host(s) naar \"{{folder}}\"", "editHostTooltip": "Bewerk de host", - "statusChecks": "Statuscontroles", - "metricsCollection": "Gegevensverzameling", - "metricsInterval": "Interval voor het verzamelen van meetgegevens", - "metricsIntervalDesc": "Hoe vaak moeten serverstatistieken worden verzameld (5s - 1u)?", "behavior": "Gedrag", "themePreview": "Themavoorbeeld", "theme": "Thema", @@ -1899,8 +1889,6 @@ "serverStats": "Serverstatistieken", "loadingServerStats": "Serverstatistieken worden geladen...", "noServerData": "Geen servergegevens beschikbaar", - "cpu": "CPU", - "ram": "RAM", "customizeLayout": "Dashboard aanpassen", "dashboardSettings": "Dashboardinstellingen", "enableDisableCards": "Kaarten in-/uitschakelen", @@ -1952,7 +1940,6 @@ "serviceLinksUrlPlaceholder": "http://192.168.1.10:8080", "serviceLinksInvalidUrl": "Voer een geldig webadres in.", "serviceLinksAddFailed": "Het toevoegen van de servicelink is mislukt.", - "disk": "Schijf", "viewServerDetails": "Servergegevens bekijken" }, "admin": { @@ -2091,7 +2078,6 @@ "sessionTimeoutRange": "Minimaal 1 uur · Maximaal 720 uur", "monitoringDefaults": "Standaardinstellingen bewaken", "statusCheck": "Statuscontrole", - "metrics": "Statistieken", "sec": "sec", "logLevel": "Logniveau", "enableGuacamole": "Schakel Guacamole in", @@ -2353,9 +2339,6 @@ "hostDefaultsSocks5Host": "Proxy-host / poort", "hostDefaultsSocks5Username": "Proxy-gebruikersnaam", "hostDefaultsSocks5Password": "Proxy-wachtwoord", - "hostDefaultsMetrics": "Hoststatistieken", - "hostDefaultsMetricsEnabled": "Statistieken inschakelen", - "hostDefaultsMetricsEnabledDesc": "Verzamel standaard CPU-, geheugen- en andere statistieken van nieuwe hosts.", "hostDefaultsStatusCheckEnabled": "Statuscontrole inschakelen", "hostDefaultsStatusCheckEnabledDesc": "Controleer standaard de online/offline-status van nieuwe hosts.", "hostDefaultsTerminal": "Terminal", diff --git a/src/ui/locales/translated/no_NO.json b/src/ui/locales/translated/no_NO.json index 86195e72a..5e162cdd6 100644 --- a/src/ui/locales/translated/no_NO.json +++ b/src/ui/locales/translated/no_NO.json @@ -268,7 +268,6 @@ "showDisk": "Vis diskbruk", "addToFolder": "Legg til widget i mappe", "noHostSelected": "Ingen vert valgt", - "metricsNotAvailable": "Målinger er ikke tilgjengelige", "selectHost": "Velg en vert...", "displayedMetrics": "Viste målinger", "metricCpu": "CPU", @@ -276,13 +275,8 @@ "metricDisk": "Disk", "metricUptime": "Oppetid", "metricSystem": "System", - "metricOs": "OS", - "metricKernel": "Kjerne", - "metricHostname": "Vertsnavn", "metricNetwork": "Nettverk", "metricProcesses": "Prosesser", - "metricProcessesTotal": "total", - "metricProcessesRunning": "løping", "categoryMonitoring": "Overvåking", "loading": "Laster inn...", "allClear": "Alt klart", @@ -775,10 +769,6 @@ "discardChanges": "Forkast endringer", "movedToFolder": "Flyttet {{count}} vert(er) til \"{{folder}}\"", "editHostTooltip": "Rediger vert", - "statusChecks": "Statuskontroller", - "metricsCollection": "Metriksamling", - "metricsInterval": "Intervall for innsamling av metrikk", - "metricsIntervalDesc": "Hvor ofte skal serverstatistikk samles inn (5 sekunder - 1 time)", "behavior": "Oppførsel", "themePreview": "Forhåndsvisning av tema", "theme": "Tema", @@ -1899,8 +1889,6 @@ "serverStats": "Serverstatistikk", "loadingServerStats": "Laster inn serverstatistikk ...", "noServerData": "Ingen serverdata tilgjengelig", - "cpu": "CPU", - "ram": "VÆR", "customizeLayout": "Tilpass dashbordet", "dashboardSettings": "Innstillinger for dashbord", "enableDisableCards": "Aktiver/deaktiver kort", @@ -1952,7 +1940,6 @@ "serviceLinksUrlPlaceholder": "http://192.168.1.10:8080", "serviceLinksInvalidUrl": "Skriv inn en gyldig nettadresse", "serviceLinksAddFailed": "Kunne ikke legge til tjenestelenke", - "disk": "Disk", "viewServerDetails": "Vis serverdetaljer" }, "admin": { @@ -2091,7 +2078,6 @@ "sessionTimeoutRange": "Min. 1 t · Maks. 720 t", "monitoringDefaults": "Overvåking av standardinnstillinger", "statusCheck": "Statussjekk", - "metrics": "Målinger", "sec": "sek", "logLevel": "Loggnivå", "enableGuacamole": "Aktiver guacamole", @@ -2353,9 +2339,6 @@ "hostDefaultsSocks5Host": "Proxy-vert / -port", "hostDefaultsSocks5Username": "Proxy-brukernavn", "hostDefaultsSocks5Password": "Proxy-passord", - "hostDefaultsMetrics": "Vertsmålinger", - "hostDefaultsMetricsEnabled": "Aktiver målinger", - "hostDefaultsMetricsEnabledDesc": "Samle inn CPU-, minne- og annen statistikk på nye verter som standard", "hostDefaultsStatusCheckEnabled": "Aktiver statussjekk", "hostDefaultsStatusCheckEnabledDesc": "Avstem online/offline-status på nye verter som standard", "hostDefaultsTerminal": "Terminal", diff --git a/src/ui/locales/translated/pl_PL.json b/src/ui/locales/translated/pl_PL.json index 04b50d248..aa75a045d 100644 --- a/src/ui/locales/translated/pl_PL.json +++ b/src/ui/locales/translated/pl_PL.json @@ -268,7 +268,6 @@ "showDisk": "Pokaż użycie dysku", "addToFolder": "Dodaj widżet do folderu", "noHostSelected": "Nie wybrano hosta", - "metricsNotAvailable": "Metryki niedostępne", "selectHost": "Wybierz hosta...", "displayedMetrics": "Wyświetlane metryki", "metricCpu": "Procesor", @@ -276,13 +275,8 @@ "metricDisk": "Dysk", "metricUptime": "Czas działania", "metricSystem": "System", - "metricOs": "System operacyjny", - "metricKernel": "Jądro", - "metricHostname": "Nazwa hosta", "metricNetwork": "Sieć", "metricProcesses": "Procesy", - "metricProcessesTotal": "łącznie", - "metricProcessesRunning": "działa", "categoryMonitoring": "Monitorowanie", "loading": "Ładowanie...", "allClear": "Bez alertów", @@ -775,10 +769,6 @@ "discardChanges": "Odrzuć zmiany", "movedToFolder": "Przeniesiono {{count}} hostów do „{{folder}}”", "editHostTooltip": "Edytuj hosta", - "statusChecks": "Sprawdzanie statusu", - "metricsCollection": "Zbieranie metryk", - "metricsInterval": "Interwał zbierania metryk", - "metricsIntervalDesc": "Jak często zbierać statystyki serwera (5 sek. - 1 godz.)", "behavior": "Zachowanie", "themePreview": "Podgląd motywu", "theme": "Motyw", @@ -1899,8 +1889,6 @@ "serverStats": "Statystyki serwera", "loadingServerStats": "Ładowanie statystyk serwera...", "noServerData": "Brak dostępnych danych serwera", - "cpu": "Procesor", - "ram": "RAM", "customizeLayout": "Dostosuj pulpit", "dashboardSettings": "Ustawienia pulpitu", "enableDisableCards": "Włącz/wyłącz karty", @@ -1952,7 +1940,6 @@ "serviceLinksUrlPlaceholder": "http://192.168.1.10:8080", "serviceLinksInvalidUrl": "Wprowadź prawidłowy adres internetowy", "serviceLinksAddFailed": "Nie udało się dodać linku do usługi", - "disk": "Dysk", "viewServerDetails": "Wyświetl szczegóły serwera" }, "admin": { @@ -2091,7 +2078,6 @@ "sessionTimeoutRange": "Min. 1 godz. · Maks. 720 godz.", "monitoringDefaults": "Domyślne ustawienia monitorowania", "statusCheck": "Sprawdzanie statusu", - "metrics": "Metryki", "sec": "sek", "logLevel": "Poziom logowania", "enableGuacamole": "Włącz Guacamole", @@ -2353,9 +2339,6 @@ "hostDefaultsSocks5Host": "Host/Port proxy", "hostDefaultsSocks5Username": "Nazwa użytkownika proxy", "hostDefaultsSocks5Password": "Hasło proxy", - "hostDefaultsMetrics": "Metryki hosta", - "hostDefaultsMetricsEnabled": "Włącz metryki", - "hostDefaultsMetricsEnabledDesc": "Domyślnie zbieraj na nowych hostach dane o procesorze, pamięci i inne statystyki", "hostDefaultsStatusCheckEnabled": "Włącz sprawdzanie statusu", "hostDefaultsStatusCheckEnabledDesc": "Domyślnie sprawdzaj status online/offline na nowych hostach", "hostDefaultsTerminal": "Terminal", diff --git a/src/ui/locales/translated/pt_BR.json b/src/ui/locales/translated/pt_BR.json index aa55c5f43..959ea6852 100644 --- a/src/ui/locales/translated/pt_BR.json +++ b/src/ui/locales/translated/pt_BR.json @@ -268,7 +268,6 @@ "showDisk": "Mostrar uso do disco", "addToFolder": "Adicionar widget à pasta", "noHostSelected": "Nenhum host selecionado", - "metricsNotAvailable": "Métricas não disponíveis", "selectHost": "Selecione um host...", "displayedMetrics": "Métricas exibidas", "metricCpu": "CPU", @@ -276,13 +275,8 @@ "metricDisk": "Disco", "metricUptime": "Tempo de atividade", "metricSystem": "Sistema", - "metricOs": "SO", - "metricKernel": "Kernel", - "metricHostname": "Nome do host", "metricNetwork": "Rede", "metricProcesses": "Processos", - "metricProcessesTotal": "total", - "metricProcessesRunning": "correndo", "categoryMonitoring": "Monitoramento", "loading": "Carregando...", "allClear": "Tudo certo", @@ -775,10 +769,6 @@ "discardChanges": "Descartar alterações", "movedToFolder": "Movidos os {{count}} hosts para \"{{folder}}\"", "editHostTooltip": "Editar anfitrião", - "statusChecks": "Verificações de status", - "metricsCollection": "Coleta de métricas", - "metricsInterval": "Intervalo de coleta de métricas", - "metricsIntervalDesc": "Com que frequência coletar estatísticas do servidor (5s - 1h)", "behavior": "Comportamento", "themePreview": "Pré-visualização do tema", "theme": "Tema", @@ -1899,8 +1889,6 @@ "serverStats": "Estatísticas do servidor", "loadingServerStats": "Carregando estatísticas do servidor...", "noServerData": "Não há dados disponíveis no servidor.", - "cpu": "CPU", - "ram": "BATER", "customizeLayout": "Personalize o painel de controle", "dashboardSettings": "Configurações do painel", "enableDisableCards": "Ativar/Desativar Cartões", @@ -1952,7 +1940,6 @@ "serviceLinksUrlPlaceholder": "http://192.168.1.10:8080", "serviceLinksInvalidUrl": "Insira um endereço web válido.", "serviceLinksAddFailed": "Falha ao adicionar o link de serviço", - "disk": "Disco", "viewServerDetails": "Ver detalhes do servidor" }, "admin": { @@ -2091,7 +2078,6 @@ "sessionTimeoutRange": "Mínimo 1h · Máximo 720h", "monitoringDefaults": "Monitoramento de padrões", "statusCheck": "Verificação de status", - "metrics": "Métricas", "sec": "seg", "logLevel": "Nível de registro", "enableGuacamole": "Ativar Guacamole", @@ -2353,9 +2339,6 @@ "hostDefaultsSocks5Host": "Host/Porta do proxy", "hostDefaultsSocks5Username": "Nome de usuário do proxy", "hostDefaultsSocks5Password": "Senha do proxy", - "hostDefaultsMetrics": "Métricas do host", - "hostDefaultsMetricsEnabled": "Ativar métricas", - "hostDefaultsMetricsEnabledDesc": "Coletar por padrão estatísticas de CPU, memória e outros dados em novos hosts.", "hostDefaultsStatusCheckEnabled": "Ativar verificação de status", "hostDefaultsStatusCheckEnabledDesc": "Verificar o status online/offline de novos hosts por padrão", "hostDefaultsTerminal": "terminal", diff --git a/src/ui/locales/translated/pt_PT.json b/src/ui/locales/translated/pt_PT.json index 00dbc70a9..1a7fed386 100644 --- a/src/ui/locales/translated/pt_PT.json +++ b/src/ui/locales/translated/pt_PT.json @@ -268,7 +268,6 @@ "showDisk": "Mostrar utilização do disco", "addToFolder": "Adicionar widget à pasta", "noHostSelected": "Nenhum anfitrião selecionado", - "metricsNotAvailable": "Métricas não disponíveis", "selectHost": "Selecionar um host...", "displayedMetrics": "Métricas Exibidas", "metricCpu": "CPU", @@ -276,13 +275,8 @@ "metricDisk": "Disco", "metricUptime": "Tempo de atividade", "metricSystem": "Sistema", - "metricOs": "SO", - "metricKernel": "Kernel", - "metricHostname": "Nome do host", "metricNetwork": "Rede", "metricProcesses": "Processos", - "metricProcessesTotal": "total", - "metricProcessesRunning": "em execução", "categoryMonitoring": "Monitorização", "loading": "A carregar...", "allClear": "Tudo em ordem", @@ -775,10 +769,6 @@ "discardChanges": "Descartar alterações", "movedToFolder": "Movido(s) {{count}} host(s) para \"{{folder}}\"", "editHostTooltip": "Editar host", - "statusChecks": "Verificações de estado", - "metricsCollection": "Recolha de métricas", - "metricsInterval": "Intervalo de recolha de métricas", - "metricsIntervalDesc": "Com que frequência recolher estatísticas do servidor (5s - 1h)", "behavior": "Comportamento", "themePreview": "Pré-visualização do tema", "theme": "Tema", @@ -1899,8 +1889,6 @@ "serverStats": "Estatísticas do Servidor", "loadingServerStats": "A carregar estatísticas do servidor...", "noServerData": "Nenhum dado do servidor disponível", - "cpu": "CPU", - "ram": "RAM", "customizeLayout": "Personalizar Dashboard", "dashboardSettings": "Definições do Dashboard", "enableDisableCards": "Ativar/Desativar Cartões", @@ -1952,7 +1940,6 @@ "serviceLinksUrlPlaceholder": "http://192.168.1.10:8080", "serviceLinksInvalidUrl": "Insira um endereço web válido", "serviceLinksAddFailed": "Falha ao adicionar link de serviço", - "disk": "Disco", "viewServerDetails": "Ver detalhes do servidor" }, "admin": { @@ -2091,7 +2078,6 @@ "sessionTimeoutRange": "Mín. 1h · Máx. 720h", "monitoringDefaults": "Padrões de Monitorização", "statusCheck": "Verificação de Estado", - "metrics": "Métricas", "sec": "seg", "logLevel": "Nível de Registo", "enableGuacamole": "Ativar Guacamole", @@ -2353,9 +2339,6 @@ "hostDefaultsSocks5Host": "Host / Porta do proxy", "hostDefaultsSocks5Username": "Nome de utilizador do proxy", "hostDefaultsSocks5Password": "Palavra-passe do proxy", - "hostDefaultsMetrics": "Métricas do Host", - "hostDefaultsMetricsEnabled": "Ativar Métricas", - "hostDefaultsMetricsEnabledDesc": "Recolher CPU, memória e outras estatísticas nos novos hosts por predefinição", "hostDefaultsStatusCheckEnabled": "Ativar Verificação de Estado", "hostDefaultsStatusCheckEnabledDesc": "Consultar o estado online/offline nos novos hosts por predefinição", "hostDefaultsTerminal": "Terminal", diff --git a/src/ui/locales/translated/ro_RO.json b/src/ui/locales/translated/ro_RO.json index c78548885..4fc6908bf 100644 --- a/src/ui/locales/translated/ro_RO.json +++ b/src/ui/locales/translated/ro_RO.json @@ -268,7 +268,6 @@ "showDisk": "Afișează utilizarea discului", "addToFolder": "Adăugați widgetul în folder", "noHostSelected": "Nicio gazdă selectată", - "metricsNotAvailable": "Indicatorii nu sunt disponibili", "selectHost": "Selectați o gazdă...", "displayedMetrics": "Metrici afișate", "metricCpu": "CPU", @@ -276,13 +275,8 @@ "metricDisk": "Disc", "metricUptime": "Timp de funcționare", "metricSystem": "Sistem", - "metricOs": "Sistem de operare", - "metricKernel": "Nucleu", - "metricHostname": "Nume gazdă", "metricNetwork": "Reţea", "metricProcesses": "Procese", - "metricProcessesTotal": "total", - "metricProcessesRunning": "funcţionare", "categoryMonitoring": "Monitorizare", "loading": "Încărcare...", "allClear": "Totul este clar", @@ -775,10 +769,6 @@ "discardChanges": "Anulează modificările", "movedToFolder": "Gazdele {{count}} au fost mutate la „{{folder}}”", "editHostTooltip": "Editați gazda", - "statusChecks": "Verificări de stare", - "metricsCollection": "Colecție de metrici", - "metricsInterval": "Interval de colectare a metricilor", - "metricsIntervalDesc": "Cât de des se colectează statisticile serverului (5s - 1h)", "behavior": "Comportament", "themePreview": "Previzualizare temă", "theme": "Temă", @@ -1899,8 +1889,6 @@ "serverStats": "Statistici server", "loadingServerStats": "Se încarcă statisticile serverului...", "noServerData": "Nu sunt disponibile date de server", - "cpu": "CPU", - "ram": "RAM", "customizeLayout": "Personalizați tabloul de bord", "dashboardSettings": "Setări tablou de bord", "enableDisableCards": "Activare/Dezactivare carduri", @@ -1952,7 +1940,6 @@ "serviceLinksUrlPlaceholder": "http://192.168.1.10:8080", "serviceLinksInvalidUrl": "Introduceți o adresă web validă", "serviceLinksAddFailed": "Nu s-a putut adăuga linkul de serviciu", - "disk": "Disc", "viewServerDetails": "Vizualizați detaliile serverului" }, "admin": { @@ -2091,7 +2078,6 @@ "sessionTimeoutRange": "Min 1 oră · Max 720 ore", "monitoringDefaults": "Monitorizarea setărilor implicite", "statusCheck": "Verificare stare", - "metrics": "Metrici", "sec": "sec", "logLevel": "Nivel de jurnal", "enableGuacamole": "Activează Guacamole", @@ -2353,9 +2339,6 @@ "hostDefaultsSocks5Host": "Gazdă / Port proxy", "hostDefaultsSocks5Username": "Nume de utilizator proxy", "hostDefaultsSocks5Password": "Parolă proxy", - "hostDefaultsMetrics": "Metrici ale gazdei", - "hostDefaultsMetricsEnabled": "Activați metricile", - "hostDefaultsMetricsEnabledDesc": "Colectați în mod implicit statistici despre procesor, memorie și alte date despre gazdele noi", "hostDefaultsStatusCheckEnabled": "Activează verificarea stării", "hostDefaultsStatusCheckEnabledDesc": "Sondarea implicită a stării online/offline pentru gazdele noi", "hostDefaultsTerminal": "Terminal", diff --git a/src/ui/locales/translated/ru_RU.json b/src/ui/locales/translated/ru_RU.json index 5d5e3968a..a70c4a60a 100644 --- a/src/ui/locales/translated/ru_RU.json +++ b/src/ui/locales/translated/ru_RU.json @@ -268,7 +268,6 @@ "showDisk": "Показывать использование диска", "addToFolder": "Добавить виджет в папку", "noHostSelected": "Хост не выбран", - "metricsNotAvailable": "Метрики недоступны", "selectHost": "Выберите хост...", "displayedMetrics": "Отображаемые метрики", "metricCpu": "ЦП", @@ -276,13 +275,8 @@ "metricDisk": "Диск", "metricUptime": "Время работы", "metricSystem": "Система", - "metricOs": "ОС", - "metricKernel": "Ядро", - "metricHostname": "Имя хоста", "metricNetwork": "Сеть", "metricProcesses": "Процессы", - "metricProcessesTotal": "всего", - "metricProcessesRunning": "запущено", "categoryMonitoring": "Мониторинг", "loading": "Загрузка...", "allClear": "Всё в порядке", @@ -775,10 +769,6 @@ "discardChanges": "Отменить изменения", "movedToFolder": "Перемещено {{count}} хост(а/ов) в \"{{folder}}\"", "editHostTooltip": "Редактировать хост", - "statusChecks": "Проверки статуса", - "metricsCollection": "Сбор метрик", - "metricsInterval": "Интервал сбора метрик", - "metricsIntervalDesc": "Как часто собирать статистику сервера (5с - 1ч)", "behavior": "Поведение", "themePreview": "Предпросмотр темы", "theme": "Тема", @@ -1899,8 +1889,6 @@ "serverStats": "Статистика сервера", "loadingServerStats": "Загрузка статистики сервера...", "noServerData": "Нет данных о сервере", - "cpu": "CPU", - "ram": "RAM", "customizeLayout": "Настроить дашборд", "dashboardSettings": "Настройки дашборда", "enableDisableCards": "Включить/отключить карточки", @@ -1952,7 +1940,6 @@ "serviceLinksUrlPlaceholder": "http://192.168.1.10:8080", "serviceLinksInvalidUrl": "Введите действительный веб-адрес", "serviceLinksAddFailed": "Не удалось добавить сервисную ссылку", - "disk": "Диск", "viewServerDetails": "Просмотреть сведения о сервере" }, "admin": { @@ -2091,7 +2078,6 @@ "sessionTimeoutRange": "Мин. 1 ч · Макс. 720 ч", "monitoringDefaults": "Мониторинг по умолчанию", "statusCheck": "Проверка состояния", - "metrics": "Метрики", "sec": "сек", "logLevel": "Уровень логирования", "enableGuacamole": "Включить Guacamole", @@ -2353,9 +2339,6 @@ "hostDefaultsSocks5Host": "Хост / порт прокси", "hostDefaultsSocks5Username": "Имя пользователя прокси", "hostDefaultsSocks5Password": "Пароль прокси", - "hostDefaultsMetrics": "Метрики хоста", - "hostDefaultsMetricsEnabled": "Включить метрики", - "hostDefaultsMetricsEnabledDesc": "По умолчанию собирать данные о ЦП, памяти и другую статистику с новых хостов", "hostDefaultsStatusCheckEnabled": "Включить проверку состояния", "hostDefaultsStatusCheckEnabledDesc": "По умолчанию опрашивать состояние новых хостов (онлайн/офлайн)", "hostDefaultsTerminal": "Терминал", diff --git a/src/ui/locales/translated/sr_SP.json b/src/ui/locales/translated/sr_SP.json index 76f30c355..3877b457b 100644 --- a/src/ui/locales/translated/sr_SP.json +++ b/src/ui/locales/translated/sr_SP.json @@ -268,7 +268,6 @@ "showDisk": "Прикажи коришћење диска", "addToFolder": "Додај виџет у фасциклу", "noHostSelected": "Није изабран домаћин", - "metricsNotAvailable": "Метрике нису доступне", "selectHost": "Изаберите хост...", "displayedMetrics": "Приказане метрике", "metricCpu": "Процесор", @@ -276,13 +275,8 @@ "metricDisk": "Диск", "metricUptime": "Време непрекидног рада", "metricSystem": "Систем", - "metricOs": "ОС", - "metricKernel": "Језгро", - "metricHostname": "Име хоста", "metricNetwork": "Мрежа", "metricProcesses": "Процеси", - "metricProcessesTotal": "укупно", - "metricProcessesRunning": "трчање", "categoryMonitoring": "Праћење", "loading": "Учитавање...", "allClear": "Све је чисто", @@ -775,10 +769,6 @@ "discardChanges": "Одбаци измене", "movedToFolder": "Премештено {{count}} хост(ова) на „{{folder}}“", "editHostTooltip": "Измени хоста", - "statusChecks": "Провере статуса", - "metricsCollection": "Колекција метрика", - "metricsInterval": "Интервал прикупљања метрика", - "metricsIntervalDesc": "Колико често прикупљати статистику сервера (5 с - 1 сат)", "behavior": "Понашање", "themePreview": "Преглед теме", "theme": "Тема", @@ -1899,8 +1889,6 @@ "serverStats": "Статистика сервера", "loadingServerStats": "Учитавање статистике сервера...", "noServerData": "Нема доступних података са сервера", - "cpu": "Процесор", - "ram": "РАМ меморија", "customizeLayout": "Прилагоди контролну таблу", "dashboardSettings": "Подешавања контролне табле", "enableDisableCards": "Омогући/онемогући картице", @@ -1952,7 +1940,6 @@ "serviceLinksUrlPlaceholder": "хттп://192.168.1.10:8080", "serviceLinksInvalidUrl": "Унесите важећу веб адресу", "serviceLinksAddFailed": "Додавање линка услуге није успело", - "disk": "Диск", "viewServerDetails": "Погледајте детаље сервера" }, "admin": { @@ -2091,7 +2078,6 @@ "sessionTimeoutRange": "Мин. 1 сат · Макс. 720 сати", "monitoringDefaults": "Праћење подразумеваних подешавања", "statusCheck": "Провера статуса", - "metrics": "Метрике", "sec": "сек", "logLevel": "Ниво логова", "enableGuacamole": "Омогући гуакамоле", @@ -2353,9 +2339,6 @@ "hostDefaultsSocks5Host": "Прокси хост / порт", "hostDefaultsSocks5Username": "Корисничко име проксија", "hostDefaultsSocks5Password": "Лозинка проксија", - "hostDefaultsMetrics": "Метрике хоста", - "hostDefaultsMetricsEnabled": "Омогући метрике", - "hostDefaultsMetricsEnabledDesc": "Подразумевано прикупљајте статистику процесора, меморије и друге статистике на новим хостовима", "hostDefaultsStatusCheckEnabled": "Омогући проверу статуса", "hostDefaultsStatusCheckEnabledDesc": "Подразумевано проверава статус анкете на мрежи/офлајн на новим хостовима", "hostDefaultsTerminal": "Терминал", diff --git a/src/ui/locales/translated/sv_SE.json b/src/ui/locales/translated/sv_SE.json index b66a96f91..ef64e5b17 100644 --- a/src/ui/locales/translated/sv_SE.json +++ b/src/ui/locales/translated/sv_SE.json @@ -268,7 +268,6 @@ "showDisk": "Visa diskanvändning", "addToFolder": "Lägg till widget i mapp", "noHostSelected": "Ingen värd vald", - "metricsNotAvailable": "Mätvärden är inte tillgängliga", "selectHost": "Välj en värd...", "displayedMetrics": "Visade mätvärden", "metricCpu": "CPU", @@ -276,13 +275,8 @@ "metricDisk": "Disk", "metricUptime": "Drifttid", "metricSystem": "System", - "metricOs": "operativsystem", - "metricKernel": "Kärna", - "metricHostname": "Värdnamn", "metricNetwork": "Nätverk", "metricProcesses": "Processer", - "metricProcessesTotal": "total", - "metricProcessesRunning": "spring", "categoryMonitoring": "Övervakning", "loading": "Belastning...", "allClear": "Faran över", @@ -775,10 +769,6 @@ "discardChanges": "Ignorera ändringar", "movedToFolder": "Flyttade {{count}} värd(ar) till \"{{folder}}\"", "editHostTooltip": "Redigera värd", - "statusChecks": "Statuskontroller", - "metricsCollection": "Metriksamling", - "metricsInterval": "Intervall för mätvärdeninsamling", - "metricsIntervalDesc": "Hur ofta serverstatistik ska samlas in (5 sekunder - 1 timme)", "behavior": "Beteende", "themePreview": "Förhandsvisning av tema", "theme": "Tema", @@ -1899,8 +1889,6 @@ "serverStats": "Serverstatistik", "loadingServerStats": "Laddar serverstatistik...", "noServerData": "Ingen serverdata tillgänglig", - "cpu": "CPU", - "ram": "RAM-minne", "customizeLayout": "Anpassa instrumentpanelen", "dashboardSettings": "Inställningar för instrumentpanelen", "enableDisableCards": "Aktivera/inaktivera kort", @@ -1952,7 +1940,6 @@ "serviceLinksUrlPlaceholder": "http://192.168.1.10:8080", "serviceLinksInvalidUrl": "Ange en giltig webbadress", "serviceLinksAddFailed": "Misslyckades med att lägga till tjänstelänk", - "disk": "Disk", "viewServerDetails": "Visa serverinformation" }, "admin": { @@ -2091,7 +2078,6 @@ "sessionTimeoutRange": "Min 1 timme · Max 720 timmar", "monitoringDefaults": "Övervakning av standardvärden", "statusCheck": "Statuskontroll", - "metrics": "Metrik", "sec": "sek", "logLevel": "Loggnivå", "enableGuacamole": "Aktivera guacamole", @@ -2353,9 +2339,6 @@ "hostDefaultsSocks5Host": "Proxyvärd/port", "hostDefaultsSocks5Username": "Proxy-användarnamn", "hostDefaultsSocks5Password": "Proxylösenord", - "hostDefaultsMetrics": "Värdstatistik", - "hostDefaultsMetricsEnabled": "Aktivera mätvärden", - "hostDefaultsMetricsEnabledDesc": "Samla in CPU-, minnes- och annan statistik på nya värdar som standard", "hostDefaultsStatusCheckEnabled": "Aktivera statuskontroll", "hostDefaultsStatusCheckEnabledDesc": "Avläs online/offline-status på nya värdar som standard", "hostDefaultsTerminal": "Terminal", diff --git a/src/ui/locales/translated/th_TH.json b/src/ui/locales/translated/th_TH.json index 8350a202b..eed50ed94 100644 --- a/src/ui/locales/translated/th_TH.json +++ b/src/ui/locales/translated/th_TH.json @@ -268,7 +268,6 @@ "showDisk": "แสดงการใช้งานดิสก์", "addToFolder": "เพิ่มวิดเจ็ตลงในโฟลเดอร์", "noHostSelected": "ไม่ได้เลือกโฮสต์", - "metricsNotAvailable": "ข้อมูลตัวชี้วัดไม่พร้อมใช้งาน", "selectHost": "เลือกโฮสต์...", "displayedMetrics": "ตัวชี้วัดที่แสดงผล", "metricCpu": "ซีพียู", @@ -276,13 +275,8 @@ "metricDisk": "ดิสก์", "metricUptime": "เวลาใช้งาน", "metricSystem": "ระบบ", - "metricOs": "โอเอส", - "metricKernel": "เคอร์เนล", - "metricHostname": "ชื่อโฮสต์", "metricNetwork": "เครือข่าย", "metricProcesses": "กระบวนการ", - "metricProcessesTotal": "ทั้งหมด", - "metricProcessesRunning": "วิ่ง", "categoryMonitoring": "การตรวจสอบ", "loading": "กำลังโหลด...", "allClear": "ทุกอย่างเรียบร้อย", @@ -775,10 +769,6 @@ "discardChanges": "ยกเลิกการเปลี่ยนแปลง", "movedToFolder": "ย้ายโฮสต์ {{count}} ไปยัง \"{{folder}}\"", "editHostTooltip": "แก้ไขโฮสต์", - "statusChecks": "การตรวจสอบสถานะ", - "metricsCollection": "การรวบรวมเมตริก", - "metricsInterval": "ช่วงเวลาการเก็บรวบรวมเมตริก", - "metricsIntervalDesc": "ควรเก็บรวบรวมสถิติเซิร์ฟเวอร์บ่อยแค่ไหน (5 วินาที - 1 ชั่วโมง)", "behavior": "พฤติกรรม", "themePreview": "ตัวอย่างธีม", "theme": "ธีม", @@ -1899,8 +1889,6 @@ "serverStats": "สถิติเซิร์ฟเวอร์", "loadingServerStats": "กำลังโหลดข้อมูลสถิติเซิร์ฟเวอร์...", "noServerData": "ไม่มีข้อมูลเซิร์ฟเวอร์", - "cpu": "ซีพียู", - "ram": "แรม", "customizeLayout": "ปรับแต่งแดชบอร์ด", "dashboardSettings": "การตั้งค่าแดชบอร์ด", "enableDisableCards": "เปิดใช้งาน/ปิดใช้งานการ์ด", @@ -1952,7 +1940,6 @@ "serviceLinksUrlPlaceholder": "http://192.168.1.10:8080", "serviceLinksInvalidUrl": "ป้อนที่อยู่เว็บที่ถูกต้อง", "serviceLinksAddFailed": "ไม่สามารถเพิ่มลิงก์บริการได้", - "disk": "ดิสก์", "viewServerDetails": "ดูรายละเอียดเซิร์ฟเวอร์" }, "admin": { @@ -2091,7 +2078,6 @@ "sessionTimeoutRange": "ขั้นต่ำ 1 ชั่วโมง · ขั้นสูงสุด 720 ชั่วโมง", "monitoringDefaults": "การตรวจสอบค่าเริ่มต้น", "statusCheck": "ตรวจสอบสถานะ", - "metrics": "ตัวชี้วัด", "sec": "วินาที", "logLevel": "ระดับบันทึก", "enableGuacamole": "เปิดใช้งาน Guacamole", @@ -2353,9 +2339,6 @@ "hostDefaultsSocks5Host": "พร็อกซีโฮสต์ / พอร์ต", "hostDefaultsSocks5Username": "ชื่อผู้ใช้พร็อกซี", "hostDefaultsSocks5Password": "รหัสผ่านพร็อกซี", - "hostDefaultsMetrics": "เมตริกโฮสต์", - "hostDefaultsMetricsEnabled": "เปิดใช้งานเมตริก", - "hostDefaultsMetricsEnabledDesc": "โดยค่าเริ่มต้น ระบบจะรวบรวมข้อมูล CPU, หน่วยความจำ และสถิติอื่นๆ บนโฮสต์ใหม่", "hostDefaultsStatusCheckEnabled": "เปิดใช้งานการตรวจสอบสถานะ", "hostDefaultsStatusCheckEnabledDesc": "โดยค่าเริ่มต้น ระบบจะตรวจสอบสถานะออนไลน์/ออฟไลน์ของโฮสต์ใหม่", "hostDefaultsTerminal": "เทอร์มินัล", diff --git a/src/ui/locales/translated/tr_TR.json b/src/ui/locales/translated/tr_TR.json index 8464738d9..b6ed8e5d9 100644 --- a/src/ui/locales/translated/tr_TR.json +++ b/src/ui/locales/translated/tr_TR.json @@ -268,7 +268,6 @@ "showDisk": "Disk Kullanımını Göster", "addToFolder": "Widget'ı klasöre ekle", "noHostSelected": "Sunucu seçilmedi", - "metricsNotAvailable": "Metrikler kullanılamıyor", "selectHost": "Bir sunucu seçin...", "displayedMetrics": "Gösterilen Metrikler", "metricCpu": "CPU", @@ -276,13 +275,8 @@ "metricDisk": "Disk", "metricUptime": "Çalışma Süresi", "metricSystem": "Sistem", - "metricOs": "İşletim Sistemi", - "metricKernel": "Çekirdek", - "metricHostname": "Ana Bilgisayar Adı", "metricNetwork": "Ağ", "metricProcesses": "İşlemler", - "metricProcessesTotal": "toplam", - "metricProcessesRunning": "çalışan", "categoryMonitoring": "İzleme", "loading": "Yükleniyor...", "allClear": "Her şey normal", @@ -775,10 +769,6 @@ "discardChanges": "Değişiklikleri iptal et", "movedToFolder": "{{count}} sunucu \"{{folder}}\" klasörüne taşındı.", "editHostTooltip": "Sunucuyu düzenle", - "statusChecks": "Durum Denetimleri", - "metricsCollection": "Ölçüm Toplama", - "metricsInterval": "Ölçüm Toplama Aralığı", - "metricsIntervalDesc": "Sunucu istatistiklerinin ne sıklıkta toplanacağı (5s - 1h)", "behavior": "Davranış", "themePreview": "Tema Önizlemesi", "theme": "Tema", @@ -1899,8 +1889,6 @@ "serverStats": "Sunucu İstatistikleri", "loadingServerStats": "Sunucu istatistikleri yükleniyor...", "noServerData": "Sunucu verisi mevcut değil", - "cpu": "CPU", - "ram": "RAM", "customizeLayout": "Panoyu Özelleştir", "dashboardSettings": "Pano Ayarları", "enableDisableCards": "Kartları Etkinleştir/Devre Dışı Bırak", @@ -1952,7 +1940,6 @@ "serviceLinksUrlPlaceholder": "http://192.168.1.10:8080", "serviceLinksInvalidUrl": "Geçerli bir web adresi girin", "serviceLinksAddFailed": "Servis bağlantısı eklenemedi", - "disk": "Disk", "viewServerDetails": "Sunucu ayrıntılarını görüntüle" }, "admin": { @@ -2091,7 +2078,6 @@ "sessionTimeoutRange": "En az 1 saat · En fazla 720 saat", "monitoringDefaults": "İzleme Varsayılanları", "statusCheck": "Durum Kontrolü", - "metrics": "Metrikler", "sec": "sn", "logLevel": "Günlük Düzeyi", "enableGuacamole": "Guacamole'yi Etkinleştir", @@ -2353,9 +2339,6 @@ "hostDefaultsSocks5Host": "Proxy Sunucusu / Bağlantı Noktası", "hostDefaultsSocks5Username": "Proxy Kullanıcı Adı", "hostDefaultsSocks5Password": "Proxy Parolası", - "hostDefaultsMetrics": "Ana Bilgisayar Ölçümleri", - "hostDefaultsMetricsEnabled": "Ölçümleri Etkinleştir", - "hostDefaultsMetricsEnabledDesc": "Yeni ana bilgisayarlarda varsayılan olarak CPU, bellek ve diğer istatistikleri topla", "hostDefaultsStatusCheckEnabled": "Durum Denetimini Etkinleştir", "hostDefaultsStatusCheckEnabledDesc": "Yeni ana bilgisayarlarda varsayılan olarak çevrimiçi/çevrimdışı durumunu sorgula", "hostDefaultsTerminal": "Terminal", diff --git a/src/ui/locales/translated/uk_UA.json b/src/ui/locales/translated/uk_UA.json index 8541198fe..eee03416e 100644 --- a/src/ui/locales/translated/uk_UA.json +++ b/src/ui/locales/translated/uk_UA.json @@ -268,7 +268,6 @@ "showDisk": "Показати використання диска", "addToFolder": "Додати віджет до папки", "noHostSelected": "Хост не вибрано", - "metricsNotAvailable": "Метрики недоступні", "selectHost": "Виберіть хоста...", "displayedMetrics": "Відображені показники", "metricCpu": "Процесор", @@ -276,13 +275,8 @@ "metricDisk": "Диск", "metricUptime": "Час безперебійної роботи", "metricSystem": "Система", - "metricOs": "ОС", - "metricKernel": "Ядро", - "metricHostname": "Ім'я хоста", "metricNetwork": "Мережа", "metricProcesses": "Процеси", - "metricProcessesTotal": "загальна сума", - "metricProcessesRunning": "біг", "categoryMonitoring": "Моніторинг", "loading": "Завантаження...", "allClear": "Все чисто", @@ -775,10 +769,6 @@ "discardChanges": "Відхилити зміни", "movedToFolder": "Переміщено {{count}} хост(ів) до \"{{folder}}\"", "editHostTooltip": "Редагувати хост", - "statusChecks": "Перевірки статусу", - "metricsCollection": "Колекція показників", - "metricsInterval": "Інтервал збору показників", - "metricsIntervalDesc": "Як часто збирати статистику сервера (5 с - 1 год)", "behavior": "Поведінка", "themePreview": "Попередній перегляд теми", "theme": "Тема", @@ -1899,8 +1889,6 @@ "serverStats": "Статистика сервера", "loadingServerStats": "Завантаження статистики сервера...", "noServerData": "Дані сервера недоступні", - "cpu": "Процесор", - "ram": "Оперативна пам'ять", "customizeLayout": "Налаштувати інформаційну панель", "dashboardSettings": "Налаштування панелі інструментів", "enableDisableCards": "Увімкнути/вимкнути картки", @@ -1952,7 +1940,6 @@ "serviceLinksUrlPlaceholder": "http://192.168.1.10:8080", "serviceLinksInvalidUrl": "Введіть дійсну веб-адресу", "serviceLinksAddFailed": "Не вдалося додати посилання на послугу", - "disk": "Диск", "viewServerDetails": "Переглянути деталі сервера" }, "admin": { @@ -2091,7 +2078,6 @@ "sessionTimeoutRange": "Мін. 1 год · Макс. 720 год", "monitoringDefaults": "Моніторинг налаштувань за замовчуванням", "statusCheck": "Перевірка стану", - "metrics": "Метрики", "sec": "сек", "logLevel": "Рівень журналу", "enableGuacamole": "Увімкнути гуакамоле", @@ -2353,9 +2339,6 @@ "hostDefaultsSocks5Host": "Проксі-хост / порт", "hostDefaultsSocks5Username": "Ім'я користувача проксі-сервера", "hostDefaultsSocks5Password": "Пароль проксі-сервера", - "hostDefaultsMetrics": "Метрики хоста", - "hostDefaultsMetricsEnabled": "Увімкнути метрики", - "hostDefaultsMetricsEnabledDesc": "Збирати статистику процесора, пам'яті та інші дані на нових хостах за замовчуванням", "hostDefaultsStatusCheckEnabled": "Увімкнути перевірку стану", "hostDefaultsStatusCheckEnabledDesc": "Опитувати статус онлайн/офлайн на нових хостах за замовчуванням", "hostDefaultsTerminal": "Термінал", diff --git a/src/ui/locales/translated/vi_VN.json b/src/ui/locales/translated/vi_VN.json index 84dfa5a9f..81a8545b9 100644 --- a/src/ui/locales/translated/vi_VN.json +++ b/src/ui/locales/translated/vi_VN.json @@ -268,7 +268,6 @@ "showDisk": "Hiển thị mức sử dụng ổ đĩa", "addToFolder": "Thêm widget vào thư mục", "noHostSelected": "Chưa chọn máy chủ", - "metricsNotAvailable": "Chỉ số không khả dụng", "selectHost": "Chọn máy chủ...", "displayedMetrics": "Chỉ số hiển thị", "metricCpu": "CPU", @@ -276,13 +275,8 @@ "metricDisk": "Đĩa", "metricUptime": "Thời gian hoạt động", "metricSystem": "Hệ thống", - "metricOs": "Hệ điều hành", - "metricKernel": "Nhân", - "metricHostname": "Tên máy chủ", "metricNetwork": "Mạng", "metricProcesses": "Tiến trình", - "metricProcessesTotal": "tổng", - "metricProcessesRunning": "đang chạy", "categoryMonitoring": "Giám sát", "loading": "Đang tải...", "allClear": "Tất cả ổn", @@ -775,10 +769,6 @@ "discardChanges": "Hủy bỏ các thay đổi", "movedToFolder": "Đã chuyển {{count}} máy chủ vào thư mục \"{{folder}}\"", "editHostTooltip": "Chỉnh sửa máy chủ", - "statusChecks": "Kiểm tra trạng thái", - "metricsCollection": "Thu thập chỉ số", - "metricsInterval": "Khoảng thời gian thu thập chỉ số", - "metricsIntervalDesc": "Tần suất thu thập thống kê máy chủ (5s - 1h)", "behavior": "Hành vi", "themePreview": "Xem trước chủ đề", "theme": "Chủ đề", @@ -1899,8 +1889,6 @@ "serverStats": "Thống kê máy chủ", "loadingServerStats": "Đang tải thống kê máy chủ...", "noServerData": "Không có dữ liệu máy chủ", - "cpu": "CPU", - "ram": "RAM", "customizeLayout": "Tùy chỉnh Bảng điều khiển", "dashboardSettings": "Cài đặt Bảng điều khiển", "enableDisableCards": "Bật/Tắt Thẻ", @@ -1952,7 +1940,6 @@ "serviceLinksUrlPlaceholder": "http://192.168.1.10:8080", "serviceLinksInvalidUrl": "Nhập địa chỉ web hợp lệ", "serviceLinksAddFailed": "Thêm liên kết dịch vụ thất bại", - "disk": "Ổ đĩa", "viewServerDetails": "Xem chi tiết máy chủ" }, "admin": { @@ -2091,7 +2078,6 @@ "sessionTimeoutRange": "Tối thiểu 1h · Tối đa 720h", "monitoringDefaults": "Mặc định giám sát", "statusCheck": "Kiểm tra trạng thái", - "metrics": "Số liệu", "sec": "giây", "logLevel": "Mức nhật ký", "enableGuacamole": "Bật Guacamole", @@ -2353,9 +2339,6 @@ "hostDefaultsSocks5Host": "Máy chủ / Cổng proxy", "hostDefaultsSocks5Username": "Tên đăng nhập proxy", "hostDefaultsSocks5Password": "Mật khẩu proxy", - "hostDefaultsMetrics": "Số liệu máy chủ", - "hostDefaultsMetricsEnabled": "Bật số liệu", - "hostDefaultsMetricsEnabledDesc": "Mặc định thu thập CPU, bộ nhớ và các số liệu khác trên máy chủ mới", "hostDefaultsStatusCheckEnabled": "Bật kiểm tra trạng thái", "hostDefaultsStatusCheckEnabledDesc": "Mặc định kiểm tra trạng thái trực tuyến/ngoại tuyến trên máy chủ mới", "hostDefaultsTerminal": "Terminal", diff --git a/src/ui/locales/translated/zh_CN.json b/src/ui/locales/translated/zh_CN.json index a27d277c0..c6d74e588 100644 --- a/src/ui/locales/translated/zh_CN.json +++ b/src/ui/locales/translated/zh_CN.json @@ -268,7 +268,6 @@ "showDisk": "显示磁盘使用情况", "addToFolder": "添加小组件到文件夹", "noHostSelected": "未选择主机", - "metricsNotAvailable": "指标不可用", "selectHost": "选择主机...", "displayedMetrics": "显示指标", "metricCpu": "CPU", @@ -276,13 +275,8 @@ "metricDisk": "磁盘", "metricUptime": "运行时间", "metricSystem": "系统", - "metricOs": "操作系统", - "metricKernel": "内核", - "metricHostname": "主机名", "metricNetwork": "网络", "metricProcesses": "进程", - "metricProcessesTotal": "总数", - "metricProcessesRunning": "运行中", "categoryMonitoring": "监控", "loading": "加载中...", "allClear": "一切正常", @@ -775,10 +769,6 @@ "discardChanges": "放弃更改", "movedToFolder": "已将 {{count}} 台主机移至 \"{{folder}}\"", "editHostTooltip": "编辑主机", - "statusChecks": "状态检查", - "metricsCollection": "指标采集", - "metricsInterval": "指标采集间隔", - "metricsIntervalDesc": "采集服务器统计信息的频率(5 秒 - 1 小时)", "behavior": "行为", "themePreview": "主题预览", "theme": "主题", @@ -1899,8 +1889,6 @@ "serverStats": "服务器统计", "loadingServerStats": "正在加载服务器统计...", "noServerData": "无服务器数据可用", - "cpu": "CPU", - "ram": "内存", "customizeLayout": "自定义仪表盘", "dashboardSettings": "仪表盘设置", "enableDisableCards": "启用/禁用卡片", @@ -1952,7 +1940,6 @@ "serviceLinksUrlPlaceholder": "http://192.168.1.10:8080", "serviceLinksInvalidUrl": "输入有效的网址", "serviceLinksAddFailed": "添加服务链接失败", - "disk": "磁盘", "viewServerDetails": "查看服务器详情" }, "admin": { @@ -2091,7 +2078,6 @@ "sessionTimeoutRange": "最短 1 小时 · 最长 720 小时", "monitoringDefaults": "监控默认值", "statusCheck": "状态检查", - "metrics": "指标", "sec": "秒", "logLevel": "日志级别", "enableGuacamole": "启用 Guacamole", @@ -2353,9 +2339,6 @@ "hostDefaultsSocks5Host": "代理主机 / 端口", "hostDefaultsSocks5Username": "代理用户名", "hostDefaultsSocks5Password": "代理密码", - "hostDefaultsMetrics": "主机指标", - "hostDefaultsMetricsEnabled": "启用指标", - "hostDefaultsMetricsEnabledDesc": "默认情况下,在新主机上收集 CPU、内存及其他统计数据", "hostDefaultsStatusCheckEnabled": "启用状态检查", "hostDefaultsStatusCheckEnabledDesc": "默认轮询新主机的在线/离线状态", "hostDefaultsTerminal": "终端", diff --git a/src/ui/locales/translated/zh_TW.json b/src/ui/locales/translated/zh_TW.json index 799f42514..11c535aaa 100644 --- a/src/ui/locales/translated/zh_TW.json +++ b/src/ui/locales/translated/zh_TW.json @@ -268,7 +268,6 @@ "showDisk": "顯示磁碟使用量", "addToFolder": "新增小工具至資料夾", "noHostSelected": "未選取主機", - "metricsNotAvailable": "效能指標無法使用", "selectHost": "選擇主機...", "displayedMetrics": "顯示的指標", "metricCpu": "CPU", @@ -276,13 +275,8 @@ "metricDisk": "磁碟", "metricUptime": "運作時間", "metricSystem": "系統", - "metricOs": "作業系統", - "metricKernel": "核心", - "metricHostname": "主機名稱", "metricNetwork": "網路", "metricProcesses": "程序", - "metricProcessesTotal": "總數", - "metricProcessesRunning": "執行中", "categoryMonitoring": "監控", "loading": "載入中...", "allClear": "一切正常", @@ -775,10 +769,6 @@ "discardChanges": "放棄更改", "movedToFolder": "已將 {{count}} 台主機移至 \"{{folder}}\"", "editHostTooltip": "編輯主機", - "statusChecks": "狀態檢查", - "metricsCollection": "指標收集", - "metricsInterval": "指標收集間隔", - "metricsIntervalDesc": "收集伺服器統計資料的頻率 (5秒 - 1小時)", "behavior": "行為", "themePreview": "主題預覽", "theme": "主題", @@ -1899,8 +1889,6 @@ "serverStats": "伺服器統計", "loadingServerStats": "正在載入伺服器統計...", "noServerData": "沒有可用的伺服器資料", - "cpu": "CPU", - "ram": "RAM", "customizeLayout": "自訂儀表板", "dashboardSettings": "儀表板設定", "enableDisableCards": "啟用/停用卡片", @@ -1952,7 +1940,6 @@ "serviceLinksUrlPlaceholder": "http://192.168.1.10:8080", "serviceLinksInvalidUrl": "輸入有效的網址", "serviceLinksAddFailed": "新增服務連結失敗", - "disk": "磁碟", "viewServerDetails": "檢視伺服器詳細資訊" }, "admin": { @@ -2091,7 +2078,6 @@ "sessionTimeoutRange": "最短 1 小時,最長 720 小時", "monitoringDefaults": "監控預設值", "statusCheck": "狀態檢查", - "metrics": "指標", "sec": "秒", "logLevel": "日誌等級", "enableGuacamole": "啟用 Guacamole", @@ -2353,9 +2339,6 @@ "hostDefaultsSocks5Host": "代理主機 / 連接埠", "hostDefaultsSocks5Username": "代理使用者名稱", "hostDefaultsSocks5Password": "代理密碼", - "hostDefaultsMetrics": "主機指標", - "hostDefaultsMetricsEnabled": "啟用指標", - "hostDefaultsMetricsEnabledDesc": "預設在新主機上收集 CPU、記憶體及其他統計資料", "hostDefaultsStatusCheckEnabled": "啟用狀態檢查", "hostDefaultsStatusCheckEnabledDesc": "預設輪詢新主機的線上/離線狀態", "hostDefaultsTerminal": "終端機", diff --git a/src/ui/main-axios.ts b/src/ui/main-axios.ts index ece2d2b67..e6f96cebd 100644 --- a/src/ui/main-axios.ts +++ b/src/ui/main-axios.ts @@ -10,7 +10,6 @@ import { getBasePath } from "@/lib/base-path"; import { isElectron } from "@/lib/electron"; import { clearTermixSessionStorage } from "@/shell/TabContext"; import type { SSHHost } from "@/types/index"; -import type { GpuMetrics } from "@/types/stats-widgets"; // ============================================================================ // RBAC TYPE DEFINITIONS @@ -74,140 +73,6 @@ export type SSHHostWithStatus = SSHHost & { status: "online" | "reachable" | "offline" | "unknown"; }; -interface CpuMetrics { - percent: number | null; - cores: number | null; - load: [number, number, number] | null; -} - -interface MemoryMetrics { - percent: number | null; - usedGiB: number | null; - totalGiB: number | null; -} - -export interface DiskFilesystem { - filesystem: string; - type: string; - mount: string; - percent: number | null; - usedHuman: string | null; - totalHuman: string | null; - availableHuman: string | null; - usedBytes: number | null; - totalBytes: number | null; - availableBytes: number | null; - label?: string; -} - -interface DiskMetrics { - percent: number | null; - usedHuman: string | null; - totalHuman: string | null; - availableHuman?: string | null; - mount?: string | null; - filesystems?: DiskFilesystem[]; -} - -export interface NetworkInterface { - name: string; - ip: string; - state: string; - rx?: string | null; - tx?: string | null; - rxBytes?: string | null; - txBytes?: string | null; - rxRateBps?: number | null; - txRateBps?: number | null; -} - -export interface ProcessInfo { - pid: string; - user: string; - cpu: string; - mem: string; - command: string; -} - -export interface LoginRecord { - user: string; - ip: string; - time: string; - status: "success" | "failed"; -} - -export interface ListeningPort { - protocol: "tcp" | "udp"; - localAddress: string; - localPort: number; - state?: string; - pid?: number; - process?: string; -} - -export interface FirewallRule { - chain: string; - target: string; - protocol: string; - source: string; - destination: string; - dport?: string; - sport?: string; - state?: string; - interface?: string; - extra?: string; -} - -export interface FirewallChain { - name: string; - policy: string; - rules: FirewallRule[]; -} - -export type ServerMetrics = { - cpu: CpuMetrics; - memory: MemoryMetrics; - disk: DiskMetrics; - network?: { interfaces?: NetworkInterface[] }; - uptime?: { seconds?: number | null; formatted?: string | null }; - system?: { - hostname?: string | null; - os?: string | null; - kernel?: string | null; - arch?: string | null; - }; - processes?: { - total?: number | null; - running?: number | null; - top?: ProcessInfo[]; - }; - login_stats?: { - recentLogins?: LoginRecord[]; - failedLogins?: LoginRecord[]; - totalLogins?: number; - uniqueIPs?: number; - }; - ports?: { - source?: "ss" | "netstat" | "none"; - ports?: ListeningPort[]; - }; - firewall?: { - type?: "iptables" | "nftables" | "none"; - status?: "active" | "inactive" | "unknown"; - chains?: FirewallChain[]; - }; - temperature?: { - source?: "sysfs" | "sensors" | "none"; - highestCelsius?: number | null; - sensors?: Array<{ - label: string; - celsius: number; - }>; - }; - gpu?: GpuMetrics; - lastChecked: string; -}; - export interface AuthResponse { success?: boolean; is_admin?: boolean; @@ -783,7 +648,7 @@ function getApiUrl(path: string, defaultPort: number): string { // PER-HOST ORIGIN ROUTING (Electron desktop only) // ============================================================================ // -// hostApi/fileManagerApi/statsApi above always point at the +// hostApi/fileManagerApi above always point at the // embedded local backend -- they're the shared, always-on instances. When a // host's connection origin resolves to "remote" (see // src/ui/lib/connection-origin.ts), the backend that actually holds that @@ -833,7 +698,6 @@ export function createRemoteOriginApiInstance(path: string): AxiosInstance { } let remoteFileManagerApi: AxiosInstance | null = null; -let remoteStatsApi: AxiosInstance | null = null; let remoteCoreApi: AxiosInstance | null = null; export function getRemoteFileManagerApi(): AxiosInstance { @@ -845,19 +709,10 @@ export function getRemoteFileManagerApi(): AxiosInstance { return remoteFileManagerApi; } -export function getRemoteStatsApi(): AxiosInstance { - if (!remoteStatsApi) { - remoteStatsApi = createRemoteOriginApiInstance("/plugin-api/host-metrics"); - } - return remoteStatsApi; -} - /** * The remote server's core routes, unprefixed. * - * Separate from getRemoteStatsApi because that one is rooted at the - * host-metrics plugin's mount point, and callers reaching core routes such as - * /sync/hosts must not inherit it. + * For core routes such as /sync/hosts and /host/status. */ export function getRemoteCoreApi(): AxiosInstance { if (!remoteCoreApi) { @@ -894,12 +749,6 @@ export function getSessionOrigin(sessionId: string): "local" | "remote" { return sessionOrigins.get(sessionId) === "remote" ? "remote" : "local"; } -export function getStatsApiForOrigin( - origin: "local" | "remote", -): AxiosInstance { - return origin === "remote" ? getRemoteStatsApi() : statsApi; -} - function initializeApiInstances() { // Host Management API (port 30001) - supports SSH, RDP, VNC, Telnet hostApi = createApiInstance(getApiUrl("/host", 30001), "HOST"); @@ -912,14 +761,6 @@ function initializeApiInstances() { "FILE_MANAGER", ); - // Host Metrics API - the host-metrics plugin, mounted on the main backend. - // Every existing /status, /metrics/*, /host-metrics/* and /proxmox-stats/* - // path still resolves, because the base carries the plugin mount point. - statsApi = createApiInstance( - getApiUrl("/plugin-api/host-metrics", 30001), - "STATS", - ); - // Authentication API (port 30001) authApi = createApiInstance(getApiUrl("", 30001), "AUTH"); @@ -953,9 +794,6 @@ export let sshHostApi: AxiosInstance; // File Manager Operations API (file-manager plugin) export let fileManagerApi: AxiosInstance; -// Host Metrics API (host-metrics plugin) -export let statsApi: AxiosInstance; - // Authentication API (port 30001) export let authApi: AxiosInstance; @@ -1544,16 +1382,10 @@ export { export { getAllServerStatuses, getServerStatusById, - getServerMetricsById, - startMetricsPolling, - stopMetricsPolling, - sendMetricsHeartbeat, - registerMetricsViewer, - unregisterMetricsViewer, - submitMetricsTOTP, refreshServerPolling, - notifyHostCreatedOrUpdated, -} from "@/api/host-metrics-status-api"; + getStatusCheckSettings, + updateStatusCheckSettings, +} from "@/api/host-status-api"; export { getHostSidebarPreferences, @@ -1568,8 +1400,6 @@ export { export { getUiPreferences, saveUiPreferences } from "@/api/ui-preferences-api"; export { - getGlobalMonitoringSettings, - updateGlobalMonitoringSettings, getLogLevel, updateLogLevel, getSessionTimeout, diff --git a/src/ui/plugin-host/bridge.ts b/src/ui/plugin-host/bridge.ts index fda329e6a..89ec7ce49 100644 --- a/src/ui/plugin-host/bridge.ts +++ b/src/ui/plugin-host/bridge.ts @@ -28,6 +28,7 @@ import { invokeAction } from "@/shell/action-registry"; import { useSshAuthProviders } from "@/hooks/useSshAuthProviders"; import { useActionSlot } from "@/hooks/use-action-slot"; import { usePluginComponent } from "./component-registry"; +import { useOptionalHostStatusEntry } from "@/lib/ServerStatusContext"; /** * Core permission groups, mirroring RESERVED_PERMISSION_PREFIXES in the SDK @@ -284,6 +285,15 @@ export const pluginHostBridge: PluginHostBridge = { ], usePluginComponent: (id) => usePluginComponent(id), + + useHostStatus: (hostId) => { + const entry = useOptionalHostStatusEntry(hostId); + if (!entry) return null; + return { + status: entry.status === "degraded" ? "unknown" : entry.status, + ...(entry.reason ? { reason: entry.reason } : {}), + }; + }, }; /** Installs the bridge. Idempotent. */ diff --git a/src/ui/plugin-host/sdk-ui.ts b/src/ui/plugin-host/sdk-ui.ts index 11ecfe54a..6b7e527a8 100644 --- a/src/ui/plugin-host/sdk-ui.ts +++ b/src/ui/plugin-host/sdk-ui.ts @@ -38,6 +38,7 @@ export * from "@/components/tooltip"; export * from "@/components/section-card"; export * from "@/components/metric-card"; export * from "@/components/charts"; +export { LineChart, type LineChartSeries } from "@/components/charts/LineChart"; export { CardGridCanvas, ColumnCountStepper, @@ -82,6 +83,12 @@ export { } from "@/lib/adaptive-polling"; export * from "@/components/sheet"; export { useConfirmation } from "@/hooks/use-confirmation"; +export { useAdaptivePolling } from "@/hooks/use-adaptive-polling"; +// Per-area display density and chart options the user picked in Appearance. +export { useAreaPreferences } from "@/contexts/UiPreferencesContext"; +// Homepage widget pieces, for plugins that register a widget. +export { WidgetTitle } from "@/features/homepage/widgets/WidgetTitle"; +export { runVisibleInterval } from "@/features/homepage/use-visible-interval"; export { useIsMobile } from "@/hooks/use-mobile"; export { PassphraseDialog } from "@/ssh/dialogs/PassphraseDialog"; export { OPKSSHDialog } from "@/ssh/dialogs/OPKSSHDialog"; diff --git a/src/ui/sidebar/AdminSettingsPanel.tsx b/src/ui/sidebar/AdminSettingsPanel.tsx index 398a9eb04..a42d3f0cd 100644 --- a/src/ui/sidebar/AdminSettingsPanel.tsx +++ b/src/ui/sidebar/AdminSettingsPanel.tsx @@ -29,8 +29,8 @@ import { updatePasswordResetAllowed, getSessionTimeout, updateSessionTimeout, - getGlobalMonitoringSettings, - updateGlobalMonitoringSettings, + getStatusCheckSettings, + updateStatusCheckSettings, getLogLevel, updateLogLevel, getOidcAutoProvision, @@ -134,7 +134,6 @@ export function AdminSettingsPanel({ const [allowPasswordReset, setAllowPasswordReset] = useState(true); const [sessionTimeout, setSessionTimeout] = useState("24"); const [statusInterval, setStatusInterval] = useState("60"); - const [metricsInterval, setMetricsInterval] = useState("30"); const [logLevel, setLogLevel] = useState("info"); const [analyticsEnabled, setAnalyticsEnabled] = useState(true); const [analyticsLocked, setAnalyticsLocked] = useState(false); @@ -359,7 +358,7 @@ export function AdminSettingsPanel({ getPasswordLoginAllowed(), getPasswordResetAllowed(), getSessionTimeout(), - getGlobalMonitoringSettings(), + getStatusCheckSettings(), getLogLevel(), getOidcAutoProvision(), getSecondFactorAfterExternalLogin(), @@ -392,7 +391,6 @@ export function AdminSettingsPanel({ setSessionTimeout(String(timeout.value.timeoutHours)); if (monitoring.status === "fulfilled") { setStatusInterval(String(monitoring.value.statusCheckInterval)); - setMetricsInterval(String(monitoring.value.metricsInterval)); } if (level.status === "fulfilled") setLogLevel(level.value.level); @@ -630,16 +628,12 @@ export function AdminSettingsPanel({ async function handleSaveMonitoring() { const status = parseInt(statusInterval, 10); - const metrics = parseInt(metricsInterval, 10); - if (isNaN(status) || isNaN(metrics)) { + if (isNaN(status) || status < 5 || status > 3600) { toast.error(t("admin.monitoringIntervalInvalid")); return; } try { - await updateGlobalMonitoringSettings({ - statusCheckInterval: status, - metricsInterval: metrics, - }); + await updateStatusCheckSettings({ statusCheckInterval: status }); toast.success(t("admin.monitoringSaved")); } catch { toast.error(t("admin.monitoringSaveFailed")); @@ -1070,8 +1064,6 @@ export function AdminSettingsPanel({ handleSaveSessionTimeout={handleSaveSessionTimeout} statusInterval={statusInterval} setStatusInterval={setStatusInterval} - metricsInterval={metricsInterval} - setMetricsInterval={setMetricsInterval} handleSaveMonitoring={handleSaveMonitoring} logLevel={logLevel} handleSaveLogLevel={handleSaveLogLevel} diff --git a/src/ui/sidebar/AdminSettingsSections.tsx b/src/ui/sidebar/AdminSettingsSections.tsx index 935bd078f..e4807ac08 100644 --- a/src/ui/sidebar/AdminSettingsSections.tsx +++ b/src/ui/sidebar/AdminSettingsSections.tsx @@ -57,8 +57,6 @@ type GeneralSettingsSectionProps = { handleSaveSessionTimeout: () => void; statusInterval: string; setStatusInterval: Dispatch>; - metricsInterval: string; - setMetricsInterval: Dispatch>; handleSaveMonitoring: () => void; logLevel: string; handleSaveLogLevel: (level: string) => void; @@ -98,8 +96,6 @@ export function AdminGeneralSettingsSection({ handleSaveSessionTimeout, statusInterval, setStatusInterval, - metricsInterval, - setMetricsInterval, handleSaveMonitoring, logLevel, handleSaveLogLevel, @@ -352,22 +348,6 @@ export function AdminGeneralSettingsSection({ {t("admin.sec")} -
- -
- -
- setMetricsInterval(e.target.value)} - className="w-20 text-sm" - /> - - {t("admin.sec")} -
+ } + > +
+ + setField("statusCheckEnabled", value)} + /> + + {form.statusCheckEnabled && ( + + + setField("statusCheckInterval", useGlobal ? null : 60) + } + /> + + )} + {form.statusCheckEnabled && form.statusCheckInterval !== null && ( + + + setField("statusCheckInterval", Number(e.target.value)) + } + className="w-20 h-7 text-xs text-right [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" + /> + + )} +
+
+ } diff --git a/src/ui/sidebar/HostManagerData.ts b/src/ui/sidebar/HostManagerData.ts index ee753a8f3..3c2a19c1c 100644 --- a/src/ui/sidebar/HostManagerData.ts +++ b/src/ui/sidebar/HostManagerData.ts @@ -150,7 +150,8 @@ export function sshHostToHost(h: SSHHostWithStatus): Host { terminalConfig: parsedTerminalConfig as Host["terminalConfig"], hasSudoPassword: !!host.hasSudoPassword || !!parsedTerminalConfig?.sudoPassword, - statsConfig: parseJson(h.statsConfig) as Host["statsConfig"], + statusCheckEnabled: h.statusCheckEnabled !== false, + statusCheckInterval: h.statusCheckInterval ?? null, forceKeyboardInteractive: h.forceKeyboardInteractive ?? false, useSocks5: h.useSocks5, socks5Host: h.socks5Host, diff --git a/src/ui/sidebar/host-export-payload.ts b/src/ui/sidebar/host-export-payload.ts index f4bb6edd4..fb1fe987b 100644 --- a/src/ui/sidebar/host-export-payload.ts +++ b/src/ui/sidebar/host-export-payload.ts @@ -57,7 +57,8 @@ export const FIELD_GROUP_KEYS: Record = { "forceKeyboardInteractive", ], advanced: [ - "statsConfig", + "statusCheckEnabled", + "statusCheckInterval", "dockerConfig", "webUiConfig", "proxmoxConfig", diff --git a/src/ui/sidebar/tree/HostItem/HostItem.test.ts b/src/ui/sidebar/tree/HostItem/HostItem.test.ts index 0a798e58a..2dd59b141 100644 --- a/src/ui/sidebar/tree/HostItem/HostItem.test.ts +++ b/src/ui/sidebar/tree/HostItem/HostItem.test.ts @@ -17,7 +17,7 @@ function makeHost(overrides: Partial = {}): Host { enableRdp: false, enableVnc: false, enableTelnet: false, - statsConfig: { statusCheckEnabled: true }, + statusCheckEnabled: true, ...overrides, } as unknown as Host; } @@ -56,15 +56,7 @@ describe("buildStatusTooltip", () => { }); it("returns 'Monitoring disabled' when status check is disabled", () => { - const host = makeHost({ - statsConfig: { - enabledWidgets: [], - statusCheckEnabled: false, - statusCheckInterval: 30, - metricsEnabled: false, - metricsInterval: 30, - }, - }); + const host = makeHost({ statusCheckEnabled: false }); const tooltip = buildStatusTooltip(host, "online", t); expect(tooltip).toBe("Monitoring disabled"); }); @@ -106,20 +98,12 @@ describe("buildStatusTooltip", () => { describe("statusCheckEnabled", () => { it("returns true when statusCheckEnabled is not set (default)", () => { - const host = makeHost({ statsConfig: undefined }); + const host = makeHost({ statusCheckEnabled: undefined }); expect(statusCheckEnabled(host)).toBe(true); }); it("returns false when statusCheckEnabled is explicitly false", () => { - const host = makeHost({ - statsConfig: { - enabledWidgets: [], - statusCheckEnabled: false, - statusCheckInterval: 30, - metricsEnabled: false, - metricsInterval: 30, - }, - }); + const host = makeHost({ statusCheckEnabled: false }); expect(statusCheckEnabled(host)).toBe(false); }); }); diff --git a/src/ui/sidebar/tree/HostItem/HostItem.tsx b/src/ui/sidebar/tree/HostItem/HostItem.tsx index b5c44639c..b00a704c2 100644 --- a/src/ui/sidebar/tree/HostItem/HostItem.tsx +++ b/src/ui/sidebar/tree/HostItem/HostItem.tsx @@ -89,7 +89,7 @@ import { shell } from "@/plugin-host/shell-bridge"; import type { TabShellCallbacks } from "@/shell/tab-registry"; export function statusCheckEnabled(host: Host): boolean { - return host.statsConfig?.statusCheckEnabled !== false; + return host.statusCheckEnabled !== false; } export function buildStatusTooltip( diff --git a/src/ui/sidebar/tree/SidebarTree.tsx b/src/ui/sidebar/tree/SidebarTree.tsx index ddb1f3fc7..c77ff4edd 100644 --- a/src/ui/sidebar/tree/SidebarTree.tsx +++ b/src/ui/sidebar/tree/SidebarTree.tsx @@ -642,7 +642,8 @@ export function SidebarTree({ name: a.name, snippetId: Number(a.snippetId), })), - statsConfig: host.statsConfig, + statusCheckEnabled: host.statusCheckEnabled, + statusCheckInterval: host.statusCheckInterval, terminalConfig: host.terminalConfig ?? null, }; const created = await createSSHHost(duplicateHost); diff --git a/src/ui/tests/api/host-status-api.test.ts b/src/ui/tests/api/host-status-api.test.ts new file mode 100644 index 000000000..ba67a635d --- /dev/null +++ b/src/ui/tests/api/host-status-api.test.ts @@ -0,0 +1,109 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const remoteCoreApiMock = vi.hoisted(() => ({ get: vi.fn() })); +const sshHostApiMock = vi.hoisted(() => ({ + get: vi.fn(), + post: vi.fn(), + put: vi.fn(), +})); +const resolveConnectionOriginMock = vi.hoisted(() => vi.fn()); + +vi.mock("@/main-axios", () => ({ + sshHostApi: sshHostApiMock, + getRemoteCoreApi: () => remoteCoreApiMock, + isElectron: () => true, + handleApiError: vi.fn(), +})); + +vi.mock("@/lib/hosts-request-cache", () => ({ + getCachedServerStatuses: (loader: () => Promise) => loader(), +})); + +vi.mock("@/lib/connection-origin", () => ({ + resolveConnectionOrigin: resolveConnectionOriginMock, +})); + +import { + getAllServerStatuses, + refreshServerPolling, + updateStatusCheckSettings, +} from "@/api/host-status-api"; + +/** The host list for "/db/host", core's statuses for "/status". */ +function hostsAndStatuses(hosts: unknown[]) { + sshHostApiMock.get.mockImplementation(async (path: string) => + path === "/status" + ? { data: { 1: { status: "online" } } } + : { data: hosts }, + ); +} + +beforeEach(() => { + vi.clearAllMocks(); + window.electronAPI = { + invoke: vi.fn(async (channel: string) => + channel === "get-remote-sync-config" + ? { serverUrl: "https://termix.example.test" } + : null, + ), + } as unknown as NonNullable; + remoteCoreApiMock.get.mockResolvedValue({ + data: { 2: { status: "reachable" } }, + }); +}); + +describe("status check origin routing", () => { + it("only asks the embedded backend to poll local-origin hosts", async () => { + hostsAndStatuses([ + { id: 1, connectionOrigin: "local" }, + { id: 2, connectionOrigin: "remote" }, + ]); + resolveConnectionOriginMock.mockImplementation(async (host) => + host.connectionOrigin === "local" ? "local" : "remote", + ); + + await expect(getAllServerStatuses()).resolves.toEqual({ + 1: { status: "online" }, + 2: { status: "reachable" }, + }); + + expect(sshHostApiMock.get).toHaveBeenCalledWith("/status", { + timeout: 2000, + params: { hostIds: "1" }, + __silentRetry: true, + }); + expect(remoteCoreApiMock.get).toHaveBeenCalledWith( + "/host/status", + expect.objectContaining({ __silentRetry: true }), + ); + }); + + it("sends an empty allowlist when every host uses the remote server", async () => { + hostsAndStatuses([{ id: 2, connectionOrigin: "remote" }]); + resolveConnectionOriginMock.mockResolvedValue("remote"); + + await getAllServerStatuses(); + + expect(sshHostApiMock.get).toHaveBeenCalledWith("/status", { + timeout: 2000, + params: { hostIds: "" }, + __silentRetry: true, + }); + }); +}); + +describe("status check routes", () => { + it("restarts checks through core", async () => { + sshHostApiMock.post.mockResolvedValue({ data: {} }); + await refreshServerPolling(); + expect(sshHostApiMock.post).toHaveBeenCalledWith("/status/refresh"); + }); + + it("saves the default interval", async () => { + sshHostApiMock.put.mockResolvedValue({ data: {} }); + await updateStatusCheckSettings({ statusCheckInterval: 45 }); + expect(sshHostApiMock.put).toHaveBeenCalledWith("/status/settings", { + statusCheckInterval: 45, + }); + }); +}); diff --git a/src/ui/tests/dashboard/HostStatusCard.test.tsx b/src/ui/tests/dashboard/HostStatusCard.test.tsx index ec794ef5d..03aed5817 100644 --- a/src/ui/tests/dashboard/HostStatusCard.test.tsx +++ b/src/ui/tests/dashboard/HostStatusCard.test.tsx @@ -23,13 +23,7 @@ describe("HostStatusCard", () => { online: false, } as Host; - render( - {}} - />, - ); + render( {}} />); const name = screen.getByText(host.name); const ip = screen.getByText(host.ip); @@ -90,13 +84,7 @@ describe("dashboard host routing", () => { ...flags, } as unknown as Host; const onOpenTab = vi.fn(); - render( - , - ); + render(); fireEvent.click(screen.getByText("Remote host")); expect(onOpenTab).toHaveBeenCalledWith(host, expected); });