From af7a006c169fef86f6459cf3c40ad5de858c9929 Mon Sep 17 00:00:00 2001 From: zaunist Date: Thu, 6 Nov 2025 00:49:09 +0800 Subject: [PATCH] fix: get notification template error --- ...e_mercenary.sql => 0004_many_avengers.sql} | 0 backend/drizzle/meta/0004_snapshot.json | 1345 +++++++ backend/drizzle/meta/_journal.json | 6 +- backend/src/db/generated-migrations.ts | 2 +- backend/src/db/schema.ts | 221 +- backend/src/repositories/notification.ts | 34 +- backend/src/services/NotificationService.ts | 16 +- frontend/src/api/notifications.ts | 521 ++- frontend/src/components/ChannelSelector.tsx | 20 +- frontend/src/pages/agents/EditAgent.tsx | 5 +- .../notifications/NotificationsConfig.tsx | 3244 +++++++++-------- frontend/src/types/agents.ts | 16 +- frontend/src/types/components.ts | 18 +- frontend/src/types/monitors.ts | 4 +- frontend/src/types/notification.ts | 28 +- pnpm-lock.yaml | 2660 +++++++++++++- 16 files changed, 6226 insertions(+), 1914 deletions(-) rename backend/drizzle/{0004_lame_mercenary.sql => 0004_many_avengers.sql} (100%) create mode 100644 backend/drizzle/meta/0004_snapshot.json diff --git a/backend/drizzle/0004_lame_mercenary.sql b/backend/drizzle/0004_many_avengers.sql similarity index 100% rename from backend/drizzle/0004_lame_mercenary.sql rename to backend/drizzle/0004_many_avengers.sql diff --git a/backend/drizzle/meta/0004_snapshot.json b/backend/drizzle/meta/0004_snapshot.json new file mode 100644 index 0000000..f6dc07e --- /dev/null +++ b/backend/drizzle/meta/0004_snapshot.json @@ -0,0 +1,1345 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "dc014861-3ddc-44b2-a659-5ec25d289558", + "prevId": "cee83298-e63f-44f4-afe2-5030efcdea26", + "tables": { + "agent_metrics_24h": { + "name": "agent_metrics_24h", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "agent_id": { + "name": "agent_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'CURRENT_TIMESTAMP'" + }, + "cpu_usage": { + "name": "cpu_usage", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cpu_cores": { + "name": "cpu_cores", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cpu_model": { + "name": "cpu_model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "memory_total": { + "name": "memory_total", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "memory_used": { + "name": "memory_used", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "memory_free": { + "name": "memory_free", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "memory_usage_rate": { + "name": "memory_usage_rate", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "load_1": { + "name": "load_1", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "load_5": { + "name": "load_5", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "load_15": { + "name": "load_15", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_metrics": { + "name": "disk_metrics", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "network_metrics": { + "name": "network_metrics", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "agent_metrics_24h_agent_timestamp_idx": { + "name": "agent_metrics_24h_agent_timestamp_idx", + "columns": [ + "agent_id", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "agent_metrics_24h_agent_id_agents_id_fk": { + "name": "agent_metrics_24h_agent_id_agents_id_fk", + "tableFrom": "agent_metrics_24h", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "agents": { + "name": "agents", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by": { + "name": "created_by", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'inactive'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_addresses": { + "name": "ip_addresses", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "os": { + "name": "os", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "keepalive": { + "name": "keepalive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "agents_token_unique": { + "name": "agents_token_unique", + "columns": [ + "token" + ], + "isUnique": true + } + }, + "foreignKeys": { + "agents_created_by_users_id_fk": { + "name": "agents_created_by_users_id_fk", + "tableFrom": "agents", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "monitor_daily_stats": { + "name": "monitor_daily_stats", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "monitor_id": { + "name": "monitor_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "date": { + "name": "date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "total_checks": { + "name": "total_checks", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "up_checks": { + "name": "up_checks", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "down_checks": { + "name": "down_checks", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "avg_response_time": { + "name": "avg_response_time", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "min_response_time": { + "name": "min_response_time", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "max_response_time": { + "name": "max_response_time", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "availability": { + "name": "availability", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "monitor_daily_stats_monitor_id_date_idx": { + "name": "monitor_daily_stats_monitor_id_date_idx", + "columns": [ + "monitor_id", + "date" + ], + "isUnique": false + } + }, + "foreignKeys": { + "monitor_daily_stats_monitor_id_monitors_id_fk": { + "name": "monitor_daily_stats_monitor_id_monitors_id_fk", + "tableFrom": "monitor_daily_stats", + "tableTo": "monitors", + "columnsFrom": [ + "monitor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "monitor_status_history_24h": { + "name": "monitor_status_history_24h", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "monitor_id": { + "name": "monitor_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'CURRENT_TIMESTAMP'" + }, + "response_time": { + "name": "response_time", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "monitor_status_history_24h_monitor_timestamp_idx": { + "name": "monitor_status_history_24h_monitor_timestamp_idx", + "columns": [ + "monitor_id", + "timestamp" + ], + "isUnique": false + }, + "monitor_status_history_24h_timestamp_idx": { + "name": "monitor_status_history_24h_timestamp_idx", + "columns": [ + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "monitor_status_history_24h_monitor_id_monitors_id_fk": { + "name": "monitor_status_history_24h_monitor_id_monitors_id_fk", + "tableFrom": "monitor_status_history_24h", + "tableTo": "monitors", + "columnsFrom": [ + "monitor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "monitors": { + "name": "monitors", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "interval": { + "name": "interval", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timeout": { + "name": "timeout", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expected_status": { + "name": "expected_status", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "headers": { + "name": "headers", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_by": { + "name": "created_by", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "active": { + "name": "active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'pending'" + }, + "response_time": { + "name": "response_time", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "last_checked": { + "name": "last_checked", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "monitors_created_by_users_id_fk": { + "name": "monitors_created_by_users_id_fk", + "tableFrom": "monitors", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_channels": { + "name": "notification_channels", + "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 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "created_by": { + "name": "created_by", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'CURRENT_TIMESTAMP'" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'CURRENT_TIMESTAMP'" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_created_by_users_id_fk": { + "name": "notification_channels_created_by_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_history": { + "name": "notification_history", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_id": { + "name": "target_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "channel_id": { + "name": "channel_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "template_id": { + "name": "template_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sent_at": { + "name": "sent_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'CURRENT_TIMESTAMP'" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_history_channel_id_notification_channels_id_fk": { + "name": "notification_history_channel_id_notification_channels_id_fk", + "tableFrom": "notification_history", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "notification_history_template_id_notification_templates_id_fk": { + "name": "notification_history_template_id_notification_templates_id_fk", + "tableFrom": "notification_history", + "tableTo": "notification_templates", + "columnsFrom": [ + "template_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_settings": { + "name": "notification_settings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'global'" + }, + "target_id": { + "name": "target_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "on_down": { + "name": "on_down", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "on_recovery": { + "name": "on_recovery", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "on_offline": { + "name": "on_offline", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "on_cpu_threshold": { + "name": "on_cpu_threshold", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cpu_threshold": { + "name": "cpu_threshold", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 90 + }, + "on_memory_threshold": { + "name": "on_memory_threshold", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "memory_threshold": { + "name": "memory_threshold", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 85 + }, + "on_disk_threshold": { + "name": "on_disk_threshold", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "disk_threshold": { + "name": "disk_threshold", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 90 + }, + "channels": { + "name": "channels", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'[]'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'CURRENT_TIMESTAMP'" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'CURRENT_TIMESTAMP'" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_settings_user_id_users_id_fk": { + "name": "notification_settings_user_id_users_id_fk", + "tableFrom": "notification_settings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_templates": { + "name": "notification_templates", + "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 + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_default": { + "name": "is_default", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_by": { + "name": "created_by", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'CURRENT_TIMESTAMP'" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'CURRENT_TIMESTAMP'" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_templates_created_by_users_id_fk": { + "name": "notification_templates_created_by_users_id_fk", + "tableFrom": "notification_templates", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "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": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "status_page_agents": { + "name": "status_page_agents", + "columns": { + "config_id": { + "name": "config_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "status_page_agents_config_id_status_page_config_id_fk": { + "name": "status_page_agents_config_id_status_page_config_id_fk", + "tableFrom": "status_page_agents", + "tableTo": "status_page_config", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "status_page_agents_agent_id_agents_id_fk": { + "name": "status_page_agents_agent_id_agents_id_fk", + "tableFrom": "status_page_agents", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "status_page_agents_config_id_agent_id_pk": { + "columns": [ + "config_id", + "agent_id" + ], + "name": "status_page_agents_config_id_agent_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "status_page_config": { + "name": "status_page_config", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'系统状态'" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'系统当前运行状态'" + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "''" + }, + "custom_css": { + "name": "custom_css", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "''" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'CURRENT_TIMESTAMP'" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'CURRENT_TIMESTAMP'" + } + }, + "indexes": {}, + "foreignKeys": { + "status_page_config_user_id_users_id_fk": { + "name": "status_page_config_user_id_users_id_fk", + "tableFrom": "status_page_config", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "status_page_monitors": { + "name": "status_page_monitors", + "columns": { + "config_id": { + "name": "config_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "monitor_id": { + "name": "monitor_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "status_page_monitors_config_id_status_page_config_id_fk": { + "name": "status_page_monitors_config_id_status_page_config_id_fk", + "tableFrom": "status_page_monitors", + "tableTo": "status_page_config", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "status_page_monitors_monitor_id_monitors_id_fk": { + "name": "status_page_monitors_monitor_id_monitors_id_fk", + "tableFrom": "status_page_monitors", + "tableTo": "monitors", + "columnsFrom": [ + "monitor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "status_page_monitors_config_id_monitor_id_pk": { + "columns": [ + "config_id", + "monitor_id" + ], + "name": "status_page_monitors_config_id_monitor_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "users_username_unique": { + "name": "users_username_unique", + "columns": [ + "username" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/backend/drizzle/meta/_journal.json b/backend/drizzle/meta/_journal.json index 8f682b1..79c8ba0 100644 --- a/backend/drizzle/meta/_journal.json +++ b/backend/drizzle/meta/_journal.json @@ -33,9 +33,9 @@ { "idx": 4, "version": "6", - "when": 1751016668000, - "tag": "0004_lame_mercenary", + "when": 1762361284317, + "tag": "0004_many_avengers", "breakpoints": true } ] -} +} \ No newline at end of file diff --git a/backend/src/db/generated-migrations.ts b/backend/src/db/generated-migrations.ts index d967fcd..1f83bb6 100644 --- a/backend/src/db/generated-migrations.ts +++ b/backend/src/db/generated-migrations.ts @@ -228,7 +228,7 @@ CREATE INDEX IF NOT EXISTS \`monitor_status_history_24h_monitor_timestamp_idx\` sql: `CREATE INDEX IF NOT EXISTS \`monitor_status_history_24h_timestamp_idx\` ON \`monitor_status_history_24h\` (\`timestamp\`);` }, { - name: "0004_lame_mercenary.sql", + name: "0004_many_avengers.sql", sql: `CREATE TABLE IF NOT EXISTS \`settings\` ( \`key\` text PRIMARY KEY NOT NULL, \`value\` text diff --git a/backend/src/db/schema.ts b/backend/src/db/schema.ts index ee3e6fe..f65a9a7 100644 --- a/backend/src/db/schema.ts +++ b/backend/src/db/schema.ts @@ -1,4 +1,11 @@ -import { int, sqliteTable, text, real, primaryKey, index } from "drizzle-orm/sqlite-core"; +import { + int, + sqliteTable, + text, + real, + primaryKey, + index, +} from "drizzle-orm/sqlite-core"; // 用户表 export const users = sqliteTable("users", { @@ -8,7 +15,7 @@ export const users = sqliteTable("users", { email: text("email"), role: text("role").notNull(), created_at: text("created_at").notNull(), - updated_at: text("updated_at").notNull() + updated_at: text("updated_at").notNull(), }); // 监控表 @@ -22,54 +29,77 @@ export const monitors = sqliteTable("monitors", { expected_status: int("expected_status").notNull(), headers: text("headers").notNull(), body: text("body"), - created_by: int("created_by").notNull().references(() => users.id), + created_by: int("created_by") + .notNull() + .references(() => users.id), active: int("active").notNull(), // SQLite 没有布尔类型,用 int 代替 status: text("status").default("pending"), response_time: int("response_time").default(0), last_checked: text("last_checked"), created_at: text("created_at").notNull(), - updated_at: text("updated_at").notNull() + updated_at: text("updated_at").notNull(), }); // 24小时监控状态历史表 -export const monitorStatusHistory24h = sqliteTable("monitor_status_history_24h", { - id: int("id").primaryKey({ autoIncrement: true }), - monitor_id: int("monitor_id").notNull().references(() => monitors.id), - status: text("status").notNull(), - timestamp: text("timestamp").default("CURRENT_TIMESTAMP"), - response_time: int("response_time"), - status_code: int("status_code"), - error: text("error") -}, (table) => ({ - // monitor_id 和 timestamp 的联合索引,用于优化按监控项和时间查询的性能 - monitorTimestampIdx: index("monitor_status_history_24h_monitor_timestamp_idx").on(table.monitor_id, table.timestamp), - // timestamp 单独索引,用于优化按时间排序和范围查询的性能 - timestampIdx: index("monitor_status_history_24h_timestamp_idx").on(table.timestamp) -})); +export const monitorStatusHistory24h = sqliteTable( + "monitor_status_history_24h", + { + id: int("id").primaryKey({ autoIncrement: true }), + monitor_id: int("monitor_id") + .notNull() + .references(() => monitors.id), + status: text("status").notNull(), + timestamp: text("timestamp").default("CURRENT_TIMESTAMP"), + response_time: int("response_time"), + status_code: int("status_code"), + error: text("error"), + }, + (table) => ({ + // monitor_id 和 timestamp 的联合索引,用于优化按监控项和时间查询的性能 + monitorTimestampIdx: index( + "monitor_status_history_24h_monitor_timestamp_idx" + ).on(table.monitor_id, table.timestamp), + // timestamp 单独索引,用于优化按时间排序和范围查询的性能 + timestampIdx: index("monitor_status_history_24h_timestamp_idx").on( + table.timestamp + ), + }) +); // 监控每日统计表 -export const monitorDailyStats = sqliteTable("monitor_daily_stats", { - id: int("id").primaryKey({ autoIncrement: true }), - monitor_id: int("monitor_id").notNull().references(() => monitors.id), - date: text("date").notNull(), - total_checks: int("total_checks").notNull().default(0), - up_checks: int("up_checks").notNull().default(0), - down_checks: int("down_checks").notNull().default(0), - avg_response_time: int("avg_response_time").default(0), - min_response_time: int("min_response_time").default(0), - max_response_time: int("max_response_time").default(0), - availability: real("availability").default(0), - created_at: text("created_at").notNull() -}, (table) => ({ - monitorDateIdx: index("monitor_daily_stats_monitor_id_date_idx").on(table.monitor_id, table.date) -})); +export const monitorDailyStats = sqliteTable( + "monitor_daily_stats", + { + id: int("id").primaryKey({ autoIncrement: true }), + monitor_id: int("monitor_id") + .notNull() + .references(() => monitors.id), + date: text("date").notNull(), + total_checks: int("total_checks").notNull().default(0), + up_checks: int("up_checks").notNull().default(0), + down_checks: int("down_checks").notNull().default(0), + avg_response_time: int("avg_response_time").default(0), + min_response_time: int("min_response_time").default(0), + max_response_time: int("max_response_time").default(0), + availability: real("availability").default(0), + created_at: text("created_at").notNull(), + }, + (table) => ({ + monitorDateIdx: index("monitor_daily_stats_monitor_id_date_idx").on( + table.monitor_id, + table.date + ), + }) +); // 客户端表 export const agents = sqliteTable("agents", { id: int("id").primaryKey({ autoIncrement: true }), name: text("name").notNull(), token: text("token").notNull().unique(), - created_by: int("created_by").notNull().references(() => users.id), + created_by: int("created_by") + .notNull() + .references(() => users.id), status: text("status").default("inactive"), created_at: text("created_at").notNull(), updated_at: text("updated_at").notNull(), @@ -81,54 +111,81 @@ export const agents = sqliteTable("agents", { }); // 客户端资源指标表 -export const agentMetrics24h = sqliteTable("agent_metrics_24h", { - id: int("id").primaryKey({ autoIncrement: true }), - agent_id: int("agent_id").notNull().references(() => agents.id), - timestamp: text("timestamp").default("CURRENT_TIMESTAMP"), - cpu_usage: real("cpu_usage"), - cpu_cores: int("cpu_cores"), - cpu_model: text("cpu_model"), - memory_total: int("memory_total"), - memory_used: int("memory_used"), - memory_free: int("memory_free"), - memory_usage_rate: real("memory_usage_rate"), - load_1: real("load_1"), - load_5: real("load_5"), - load_15: real("load_15"), - disk_metrics: text("disk_metrics"), - network_metrics: text("network_metrics") -}, (table) => ({ - // agent_id 和 timestamp 的联合索引,用于优化按代理和时间查询的性能 - agentTimestampIdx: index("agent_metrics_24h_agent_timestamp_idx").on(table.agent_id, table.timestamp) -})); +export const agentMetrics24h = sqliteTable( + "agent_metrics_24h", + { + id: int("id").primaryKey({ autoIncrement: true }), + agent_id: int("agent_id") + .notNull() + .references(() => agents.id), + timestamp: text("timestamp").default("CURRENT_TIMESTAMP"), + cpu_usage: real("cpu_usage"), + cpu_cores: int("cpu_cores"), + cpu_model: text("cpu_model"), + memory_total: int("memory_total"), + memory_used: int("memory_used"), + memory_free: int("memory_free"), + memory_usage_rate: real("memory_usage_rate"), + load_1: real("load_1"), + load_5: real("load_5"), + load_15: real("load_15"), + disk_metrics: text("disk_metrics"), + network_metrics: text("network_metrics"), + }, + (table) => ({ + // agent_id 和 timestamp 的联合索引,用于优化按代理和时间查询的性能 + agentTimestampIdx: index("agent_metrics_24h_agent_timestamp_idx").on( + table.agent_id, + table.timestamp + ), + }) +); // 状态页配置表 export const statusPageConfig = sqliteTable("status_page_config", { id: int("id").primaryKey({ autoIncrement: true }), - user_id: int("user_id").notNull().references(() => users.id), + user_id: int("user_id") + .notNull() + .references(() => users.id), title: text("title").notNull().default("系统状态"), description: text("description").default("系统当前运行状态"), logo_url: text("logo_url").default(""), custom_css: text("custom_css").default(""), created_at: text("created_at").default("CURRENT_TIMESTAMP"), - updated_at: text("updated_at").default("CURRENT_TIMESTAMP") + updated_at: text("updated_at").default("CURRENT_TIMESTAMP"), }); // 状态页监控项关联表 -export const statusPageMonitors = sqliteTable("status_page_monitors", { - config_id: int("config_id").notNull().references(() => statusPageConfig.id, { onDelete: "cascade" }), - monitor_id: int("monitor_id").notNull().references(() => monitors.id, { onDelete: "cascade" }), -}, (table) => ({ - pk: primaryKey({ columns: [table.config_id, table.monitor_id] }) -})); +export const statusPageMonitors = sqliteTable( + "status_page_monitors", + { + config_id: int("config_id") + .notNull() + .references(() => statusPageConfig.id, { onDelete: "cascade" }), + monitor_id: int("monitor_id") + .notNull() + .references(() => monitors.id, { onDelete: "cascade" }), + }, + (table) => ({ + pk: primaryKey({ columns: [table.config_id, table.monitor_id] }), + }) +); // 状态页客户端关联表 -export const statusPageAgents = sqliteTable("status_page_agents", { - config_id: int("config_id").notNull().references(() => statusPageConfig.id, { onDelete: "cascade" }), - agent_id: int("agent_id").notNull().references(() => agents.id, { onDelete: "cascade" }), -}, (table) => ({ - pk: primaryKey({ columns: [table.config_id, table.agent_id] }) -})); +export const statusPageAgents = sqliteTable( + "status_page_agents", + { + config_id: int("config_id") + .notNull() + .references(() => statusPageConfig.id, { onDelete: "cascade" }), + agent_id: int("agent_id") + .notNull() + .references(() => agents.id, { onDelete: "cascade" }), + }, + (table) => ({ + pk: primaryKey({ columns: [table.config_id, table.agent_id] }), + }) +); // 通知渠道表 export const notificationChannels = sqliteTable("notification_channels", { @@ -137,9 +194,11 @@ export const notificationChannels = sqliteTable("notification_channels", { type: text("type").notNull(), config: text("config").notNull(), enabled: int("enabled").notNull().default(1), - created_by: int("created_by").notNull().references(() => users.id), + created_by: int("created_by") + .notNull() + .references(() => users.id), created_at: text("created_at").default("CURRENT_TIMESTAMP"), - updated_at: text("updated_at").default("CURRENT_TIMESTAMP") + updated_at: text("updated_at").default("CURRENT_TIMESTAMP"), }); // 通知模板表 @@ -150,15 +209,19 @@ export const notificationTemplates = sqliteTable("notification_templates", { subject: text("subject").notNull(), content: text("content").notNull(), is_default: int("is_default").notNull().default(0), - created_by: int("created_by").notNull().references(() => users.id), + created_by: int("created_by") + .notNull() + .references(() => users.id), created_at: text("created_at").default("CURRENT_TIMESTAMP"), - updated_at: text("updated_at").default("CURRENT_TIMESTAMP") + updated_at: text("updated_at").default("CURRENT_TIMESTAMP"), }); // 通知设置表 export const notificationSettings = sqliteTable("notification_settings", { id: int("id").primaryKey({ autoIncrement: true }), - user_id: int("user_id").notNull().references(() => users.id), + user_id: int("user_id") + .notNull() + .references(() => users.id), target_type: text("target_type").notNull().default("global"), target_id: int("target_id"), enabled: int("enabled").notNull().default(1), @@ -173,7 +236,7 @@ export const notificationSettings = sqliteTable("notification_settings", { disk_threshold: int("disk_threshold").notNull().default(90), channels: text("channels").default("[]"), created_at: text("created_at").default("CURRENT_TIMESTAMP"), - updated_at: text("updated_at").default("CURRENT_TIMESTAMP") + updated_at: text("updated_at").default("CURRENT_TIMESTAMP"), }); // 通知历史记录表 @@ -181,12 +244,16 @@ export const notificationHistory = sqliteTable("notification_history", { id: int("id").primaryKey({ autoIncrement: true }), type: text("type").notNull(), target_id: int("target_id"), - channel_id: int("channel_id").notNull().references(() => notificationChannels.id), - template_id: int("template_id").notNull().references(() => notificationTemplates.id), + channel_id: int("channel_id") + .notNull() + .references(() => notificationChannels.id), + template_id: int("template_id") + .notNull() + .references(() => notificationTemplates.id), status: text("status").notNull(), content: text("content").notNull(), error: text("error"), - sent_at: text("sent_at").default("CURRENT_TIMESTAMP") + sent_at: text("sent_at").default("CURRENT_TIMESTAMP"), }); // 新增:应用设置表 diff --git a/backend/src/repositories/notification.ts b/backend/src/repositories/notification.ts index 5385d75..86903fa 100644 --- a/backend/src/repositories/notification.ts +++ b/backend/src/repositories/notification.ts @@ -182,15 +182,35 @@ export const updateNotificationTemplate = async ( Omit > ): Promise => { + const updateData: Partial = {}; + + if (template.name !== undefined) { + updateData.name = template.name; + } + + if (template.type !== undefined) { + updateData.type = template.type; + } + + if (template.subject !== undefined) { + updateData.subject = template.subject; + } + + if (template.content !== undefined) { + updateData.content = template.content; + } + + if (template.is_default !== undefined) { + updateData.is_default = template.is_default ? 1 : 0; + } + + if (Object.keys(updateData).length === 0) { + return true; // 没有需要更新的字段时视为成功 + } + const result = await db .update(notificationTemplates) - .set({ - name: template.name, - type: template.type, - subject: template.subject, - content: template.content, - is_default: template.is_default ? 1 : 0, - }) + .set(updateData) .where(and(eq(notificationTemplates.id, id), eq(notificationTemplates.created_by, userId))); return result.success; // fix: 返回 success 布尔值 diff --git a/backend/src/services/NotificationService.ts b/backend/src/services/NotificationService.ts index eec9137..f2e164a 100644 --- a/backend/src/services/NotificationService.ts +++ b/backend/src/services/NotificationService.ts @@ -677,13 +677,23 @@ export async function sendNotification( const templates = await repositories.getNotificationTemplates(userId); console.log(`[发送通知] 获取到${templates.length}个通知模板`); - const defaultTemplate = templates.find( + let defaultTemplate = templates.find( (t) => t.is_default && t.type === type ); if (!defaultTemplate) { - console.error(`[发送通知] 找不到类型为${type}的默认通知模板`); - return { success: false, results: [] }; + const fallbackTemplate = templates.find((t) => t.type === type); + + if (!fallbackTemplate) { + console.error(`[发送通知] 找不到类型为${type}的任何通知模板`); + return { success: false, results: [] }; + } + + console.warn( + `[发送通知] 找不到类型为${type}的默认通知模板,使用ID=${fallbackTemplate.id}作为回退模板` + ); + + defaultTemplate = fallbackTemplate; } console.log( diff --git a/frontend/src/api/notifications.ts b/frontend/src/api/notifications.ts index f154bd4..e58e272 100644 --- a/frontend/src/api/notifications.ts +++ b/frontend/src/api/notifications.ts @@ -1,187 +1,313 @@ import api from "./client"; +import type { + NotificationChannel, + NotificationConfig, + NotificationTemplate, +} from "../types/notification"; -// 通知渠道类型 -export interface NotificationChannel { - id: string; +type BackendNotificationChannel = { + id: number; name: string; type: string; - config: Record; - enabled: boolean; -} + config: string | Record | null; + enabled: number | boolean; + created_by?: number; + created_at?: string; + updated_at?: string; +}; -// 通知模板类型 -export interface NotificationTemplate { - id: string; +type BackendNotificationTemplate = { + id: number; name: string; type: string; subject: string; content: string; - isDefault: boolean; -} + is_default: number | boolean; + created_by?: number; + created_at?: string; + updated_at?: string; +}; -// 通知设置类型 -export interface NotificationSettings { - monitors: { - enabled: boolean; - onDown: boolean; - onRecovery: boolean; - channels: string[]; - }; - agents: { - enabled: boolean; - onOffline: boolean; - onRecovery: boolean; - onCpuThreshold: boolean; - cpuThreshold: number; - onMemoryThreshold: boolean; - memoryThreshold: number; - onDiskThreshold: boolean; - diskThreshold: number; - channels: string[]; - }; - specificMonitors: { - [monitorId: string]: { - enabled: boolean; - onDown: boolean; - onRecovery: boolean; - channels: string[]; - }; - }; - specificAgents: { - [agentId: string]: { - enabled: boolean; - onOffline: boolean; - onRecovery: boolean; - onCpuThreshold: boolean; - cpuThreshold: number; - onMemoryThreshold: boolean; - memoryThreshold: number; - onDiskThreshold: boolean; - diskThreshold: number; - channels: string[]; - }; - }; -} +export type NotificationSettings = NotificationConfig["settings"]; -// 通知配置响应类型 export interface NotificationConfigResponse { success: boolean; message?: string; - data?: { - channels: NotificationChannel[]; - templates: NotificationTemplate[]; - settings: NotificationSettings; - }; + data?: NotificationConfig; } +const parseChannelConfig = ( + config: BackendNotificationChannel["config"] +): Record => { + if (!config) { + return {}; + } + + if (typeof config === "string") { + try { + const parsed = JSON.parse(config); + if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { + return parsed as Record; + } + } catch (error) { + console.error("解析通知渠道配置失败:", error); + } + return {}; + } + + if (typeof config === "object" && !Array.isArray(config)) { + return config as Record; + } + + return {}; +}; + +const normalizeBoolean = (value: unknown, fallback = false): boolean => { + if (typeof value === "boolean") { + return value; + } + + if (typeof value === "number") { + return value !== 0; + } + + if (typeof value === "string") { + return value === "1" || value.toLowerCase() === "true"; + } + + return fallback; +}; + +const normalizeNumber = (value: unknown, fallback: number): number => { + const num = Number(value); + return Number.isFinite(num) ? num : fallback; +}; + +const normalizeChannelIds = (value: unknown): number[] => { + if (!value) { + return []; + } + + if (typeof value === "string") { + try { + const parsed = JSON.parse(value); + if (Array.isArray(parsed)) { + return parsed + .map((item) => Number(item)) + .filter((item) => Number.isInteger(item)); + } + } catch (error) { + console.error("解析通知渠道ID列表失败:", error); + return []; + } + } + + if (Array.isArray(value)) { + return value + .map((item) => Number(item)) + .filter((item) => Number.isInteger(item)); + } + + return []; +}; + +const transformChannel = ( + channel: BackendNotificationChannel +): NotificationChannel => ({ + id: Number(channel.id), + name: channel.name, + type: channel.type, + config: parseChannelConfig(channel.config), + enabled: normalizeBoolean(channel.enabled, true), + createdBy: channel.created_by, + createdAt: channel.created_at, + updatedAt: channel.updated_at, +}); + +const transformTemplate = ( + template: BackendNotificationTemplate +): NotificationTemplate => ({ + id: Number(template.id), + name: template.name, + type: template.type, + subject: template.subject, + content: template.content, + isDefault: normalizeBoolean(template.is_default, false), + createdBy: template.created_by, + createdAt: template.created_at, + updatedAt: template.updated_at, +}); + +const normalizeSettings = (settings: any): NotificationSettings => { + const normalized: NotificationSettings = { + monitors: { + enabled: false, + onDown: false, + onRecovery: false, + channels: [], + }, + agents: { + enabled: false, + onOffline: false, + onRecovery: false, + onCpuThreshold: false, + cpuThreshold: 90, + onMemoryThreshold: false, + memoryThreshold: 85, + onDiskThreshold: false, + diskThreshold: 90, + channels: [], + }, + specificMonitors: {}, + specificAgents: {}, + }; + + if (!settings) { + return normalized; + } + + if (settings.monitors) { + normalized.monitors = { + enabled: normalizeBoolean(settings.monitors.enabled), + onDown: normalizeBoolean(settings.monitors.onDown ?? settings.monitors.on_down), + onRecovery: normalizeBoolean( + settings.monitors.onRecovery ?? settings.monitors.on_recovery + ), + channels: normalizeChannelIds(settings.monitors.channels), + }; + } + + if (settings.agents) { + normalized.agents = { + enabled: normalizeBoolean(settings.agents.enabled), + onOffline: normalizeBoolean(settings.agents.onOffline ?? settings.agents.on_offline), + onRecovery: normalizeBoolean( + settings.agents.onRecovery ?? settings.agents.on_recovery + ), + onCpuThreshold: normalizeBoolean( + settings.agents.onCpuThreshold ?? settings.agents.on_cpu_threshold + ), + cpuThreshold: normalizeNumber(settings.agents.cpuThreshold ?? settings.agents.cpu_threshold, 90), + onMemoryThreshold: normalizeBoolean( + settings.agents.onMemoryThreshold ?? settings.agents.on_memory_threshold + ), + memoryThreshold: normalizeNumber( + settings.agents.memoryThreshold ?? settings.agents.memory_threshold, + 85 + ), + onDiskThreshold: normalizeBoolean( + settings.agents.onDiskThreshold ?? settings.agents.on_disk_threshold + ), + diskThreshold: normalizeNumber( + settings.agents.diskThreshold ?? settings.agents.disk_threshold, + 90 + ), + channels: normalizeChannelIds(settings.agents.channels), + }; + } + + if (settings.specificMonitors) { + Object.entries(settings.specificMonitors).forEach( + ([monitorId, monitorSetting]: [string, any]) => { + normalized.specificMonitors[monitorId] = { + enabled: normalizeBoolean(monitorSetting?.enabled), + onDown: normalizeBoolean( + monitorSetting?.onDown ?? monitorSetting?.on_down + ), + onRecovery: normalizeBoolean( + monitorSetting?.onRecovery ?? monitorSetting?.on_recovery + ), + channels: normalizeChannelIds(monitorSetting?.channels), + }; + } + ); + } + + if (settings.specificAgents) { + Object.entries(settings.specificAgents).forEach( + ([agentId, agentSetting]: [string, any]) => { + normalized.specificAgents[agentId] = { + enabled: normalizeBoolean(agentSetting?.enabled), + onOffline: normalizeBoolean( + agentSetting?.onOffline ?? agentSetting?.on_offline + ), + onRecovery: normalizeBoolean( + agentSetting?.onRecovery ?? agentSetting?.on_recovery + ), + onCpuThreshold: normalizeBoolean( + agentSetting?.onCpuThreshold ?? agentSetting?.on_cpu_threshold + ), + cpuThreshold: normalizeNumber( + agentSetting?.cpuThreshold ?? agentSetting?.cpu_threshold, + normalized.agents.cpuThreshold + ), + onMemoryThreshold: normalizeBoolean( + agentSetting?.onMemoryThreshold ?? agentSetting?.on_memory_threshold + ), + memoryThreshold: normalizeNumber( + agentSetting?.memoryThreshold ?? agentSetting?.memory_threshold, + normalized.agents.memoryThreshold + ), + onDiskThreshold: normalizeBoolean( + agentSetting?.onDiskThreshold ?? agentSetting?.on_disk_threshold + ), + diskThreshold: normalizeNumber( + agentSetting?.diskThreshold ?? agentSetting?.disk_threshold, + normalized.agents.diskThreshold + ), + channels: normalizeChannelIds(agentSetting?.channels), + }; + } + ); + } + + return normalized; +}; + // 获取完整的通知配置 export const getNotificationConfig = async (): Promise => { try { - const response = await api.get( - "/api/notifications" - ); + const response = await api.get<{ + success: boolean; + message?: string; + data?: { + channels?: BackendNotificationChannel[]; + templates?: BackendNotificationTemplate[]; + settings?: any; + }; + }>("/api/notifications"); - // 如果响应成功并且有数据,处理数据格式 - if (response.data.success && response.data.data) { - // 处理渠道配置 - 确保config字段是对象而不是字符串 - if (response.data.data.channels) { - response.data.data.channels = response.data.data.channels.map( - (channel) => { - // 使用类型断言访问后端特有属性 - const backendChannel = channel as any; - return { - id: channel.id, - name: channel.name, - type: channel.type, - // 将字符串类型的config解析为对象 - config: - typeof backendChannel.config === "string" - ? JSON.parse(backendChannel.config) - : backendChannel.config, - enabled: !!backendChannel.enabled, - }; - } - ); - } + const backendData = response.data.data; - // 处理模板配置 - 确保is_default是布尔类型 - if (response.data.data.templates) { - response.data.data.templates = response.data.data.templates.map( - (template) => { - // 使用类型断言访问后端特有属性 - const backendTemplate = template as any; - return { - id: template.id, - name: template.name, - type: template.type, - subject: template.subject, - content: template.content, - isDefault: !!backendTemplate.is_default, - }; - } - ); - } - - // 确保settings中的所有布尔字段是真正的布尔类型而不是0/1 - if (response.data.data.settings) { - const { settings } = response.data.data; - - // 处理全局监控设置 - if (settings.monitors) { - settings.monitors = { - ...settings.monitors, - enabled: !!settings.monitors.enabled, - onDown: !!settings.monitors.onDown, - onRecovery: !!settings.monitors.onRecovery, - }; - } - - // 处理全局客户端设置 - if (settings.agents) { - settings.agents = { - ...settings.agents, - enabled: !!settings.agents.enabled, - onOffline: !!settings.agents.onOffline, - onRecovery: !!settings.agents.onRecovery, - onCpuThreshold: !!settings.agents.onCpuThreshold, - onMemoryThreshold: !!settings.agents.onMemoryThreshold, - onDiskThreshold: !!settings.agents.onDiskThreshold, - }; - } - - // 处理特定监控设置 - if (settings.specificMonitors) { - Object.keys(settings.specificMonitors).forEach((monitorId) => { - const monitorSetting = settings.specificMonitors[monitorId]; - settings.specificMonitors[monitorId] = { - ...monitorSetting, - enabled: !!monitorSetting.enabled, - onDown: !!monitorSetting.onDown, - onRecovery: !!monitorSetting.onRecovery, - }; - }); - } - - // 处理特定客户端设置 - if (settings.specificAgents) { - Object.keys(settings.specificAgents).forEach((agentId) => { - const agentSetting = settings.specificAgents[agentId]; - settings.specificAgents[agentId] = { - ...agentSetting, - enabled: !!agentSetting.enabled, - onOffline: !!agentSetting.onOffline, - onRecovery: !!agentSetting.onRecovery, - onCpuThreshold: !!agentSetting.onCpuThreshold, - onMemoryThreshold: !!agentSetting.onMemoryThreshold, - onDiskThreshold: !!agentSetting.onDiskThreshold, - }; - }); - } - } + if (!backendData) { + return { + success: response.data.success, + message: response.data.message, + }; } - return response.data; + const channels = Array.isArray(backendData.channels) + ? (backendData.channels as BackendNotificationChannel[]).map( + transformChannel + ) + : []; + + const templates = Array.isArray(backendData.templates) + ? (backendData.templates as BackendNotificationTemplate[]).map( + transformTemplate + ) + : []; + + return { + success: response.data.success, + message: response.data.message, + data: { + channels, + templates, + settings: normalizeSettings(backendData.settings), + }, + }; } catch (error) { console.error("获取通知配置失败:", error); return { @@ -201,13 +327,17 @@ export const getNotificationChannels = async (): Promise<{ const response = await api.get<{ success: boolean; message?: string; - data?: NotificationChannel[]; + data?: BackendNotificationChannel[]; }>("/api/notifications/channels"); + const channels = Array.isArray(response.data.data) + ? response.data.data.map(transformChannel) + : []; + return { success: response.data.success, message: response.data.message, - channels: response.data.data, + channels, }; } catch (error) { console.error("获取通知渠道失败:", error); @@ -228,13 +358,17 @@ export const getNotificationTemplates = async (): Promise<{ const response = await api.get<{ success: boolean; message?: string; - data?: NotificationTemplate[]; + data?: BackendNotificationTemplate[]; }>("/api/notifications/templates"); + const templates = Array.isArray(response.data.data) + ? response.data.data.map(transformTemplate) + : []; + return { success: response.data.success, message: response.data.message, - templates: response.data.data, + templates, }; } catch (error) { console.error("获取通知模板失败:", error); @@ -357,17 +491,17 @@ export const saveNotificationSettings = async ( // 创建通知渠道 export const createNotificationChannel = async ( - channel: Omit + channel: Omit ): Promise<{ success: boolean; message?: string; - channelId?: string; + channelId?: number; }> => { try { const response = await api.post<{ success: boolean; message?: string; - data?: { id: string }; + data?: { id: number }; }>("/api/notifications/channels", channel); return { @@ -386,17 +520,25 @@ export const createNotificationChannel = async ( // 更新通知渠道 export const updateNotificationChannel = async ( - id: string, - channel: Partial + id: number, + channel: Partial< + Omit + > ): Promise<{ success: boolean; message?: string; }> => { try { + const payload: Record = {}; + if (channel.name !== undefined) payload.name = channel.name; + if (channel.type !== undefined) payload.type = channel.type; + if (channel.config !== undefined) payload.config = channel.config; + if (channel.enabled !== undefined) payload.enabled = channel.enabled; + const response = await api.put<{ success: boolean; message?: string; - }>(`/api/notifications/channels/${id}`, channel); + }>(`/api/notifications/channels/${id}`, payload); return response.data; } catch (error) { @@ -410,7 +552,7 @@ export const updateNotificationChannel = async ( // 删除通知渠道 export const deleteNotificationChannel = async ( - id: string + id: number ): Promise<{ success: boolean; message?: string; @@ -433,18 +575,26 @@ export const deleteNotificationChannel = async ( // 创建通知模板 export const createNotificationTemplate = async ( - template: Omit + template: Omit ): Promise<{ success: boolean; message?: string; - templateId?: string; + templateId?: number; }> => { try { + const payload = { + name: template.name, + type: template.type, + subject: template.subject, + content: template.content, + is_default: template.isDefault, + }; + const response = await api.post<{ success: boolean; message?: string; - data?: { id: string }; - }>("/api/notifications/templates", template); + data?: { id: number }; + }>("/api/notifications/templates", payload); return { success: response.data.success, @@ -462,16 +612,27 @@ export const createNotificationTemplate = async ( // 更新通知模板 export const updateNotificationTemplate = async ( - id: string, - template: Partial + id: number, + template: Partial< + Omit + > ): Promise<{ success: boolean; message?: string; }> => { try { - const response = await api.put( + const payload: Record = {}; + if (template.name !== undefined) payload.name = template.name; + if (template.type !== undefined) payload.type = template.type; + if (template.subject !== undefined) payload.subject = template.subject; + if (template.content !== undefined) payload.content = template.content; + if (template.isDefault !== undefined) { + payload.is_default = template.isDefault; + } + + const response = await api.put<{ success: boolean; message?: string }>( `/api/notifications/templates/${id}`, - template + payload ); return response.data; } catch (error) { @@ -485,7 +646,7 @@ export const updateNotificationTemplate = async ( // 删除通知模板 export const deleteNotificationTemplate = async ( - id: string + id: number ): Promise<{ success: boolean; message?: string; diff --git a/frontend/src/components/ChannelSelector.tsx b/frontend/src/components/ChannelSelector.tsx index 657bb2e..a3486f0 100644 --- a/frontend/src/components/ChannelSelector.tsx +++ b/frontend/src/components/ChannelSelector.tsx @@ -3,23 +3,7 @@ import { Box, Flex, Text } from "@radix-ui/themes"; import { CheckIcon } from "@radix-ui/react-icons"; import { useTranslation } from "react-i18next"; import { createPortal } from "react-dom"; - -// 通知渠道类型定义 -export interface NotificationChannel { - id: string; - name: string; - type: string; - config?: any; - enabled?: boolean; -} - -// 组件属性接口 -export interface ChannelSelectorProps { - channels: NotificationChannel[]; - selectedChannelIds: string[]; - onChange: (channelIds: string[]) => void; - placeholder?: string; -} +import { ChannelSelectorProps } from "@/types/components"; /** * 通知渠道选择器组件 @@ -61,7 +45,7 @@ const ChannelSelector = ({ }; // 处理选项点击 - const handleItemClick = (channelId: string) => { + const handleItemClick = (channelId: number) => { // 切换选择状态 const isSelected = selectedChannelIds.includes(channelId); const newSelectedIds = isSelected diff --git a/frontend/src/pages/agents/EditAgent.tsx b/frontend/src/pages/agents/EditAgent.tsx index fe560ed..ca701d8 100644 --- a/frontend/src/pages/agents/EditAgent.tsx +++ b/frontend/src/pages/agents/EditAgent.tsx @@ -6,6 +6,7 @@ import { ArrowLeftIcon } from "@radix-ui/react-icons"; import { getAgent, updateAgent } from "../../api/agents"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; +import type { Agent } from "../../types/agents"; const EditAgent = () => { const navigate = useNavigate(); @@ -14,7 +15,7 @@ const EditAgent = () => { const [notFound, setNotFound] = useState(false); const [formData, setFormData] = useState<{ name: string; - status: "active" | "inactive" | "connecting" | "unknown"; + status: Agent["status"]; }>({ name: "", status: "inactive", @@ -37,7 +38,7 @@ const EditAgent = () => { setFormData({ name: agent.name, - status: agent.status || "inactive", + status: agent.status ?? "inactive", }); } else { setNotFound(true); diff --git a/frontend/src/pages/notifications/NotificationsConfig.tsx b/frontend/src/pages/notifications/NotificationsConfig.tsx index 30d0d3c..a18d615 100644 --- a/frontend/src/pages/notifications/NotificationsConfig.tsx +++ b/frontend/src/pages/notifications/NotificationsConfig.tsx @@ -1,32 +1,32 @@ import { useState, useEffect, useRef } from "react"; import { - Box, - Flex, - Heading, - Text, - TextField, - Container, - TextArea, + Box, + Flex, + Heading, + Text, + TextField, + Container, + TextArea, } from "@radix-ui/themes"; import { - Button, - Card, - Tabs, - TabsContent, - TabsList, - TabsTrigger, - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogTitle, - Select, - SelectItem, - SelectTrigger, - SelectContent, - SelectValue, - Switch, + Button, + Card, + Tabs, + TabsContent, + TabsList, + TabsTrigger, + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogTitle, + Select, + SelectItem, + SelectTrigger, + SelectContent, + SelectValue, + Switch, } from "@/components/ui"; import { BellIcon, PlusIcon } from "@radix-ui/react-icons"; @@ -37,42 +37,260 @@ import { Monitor } from "../../types/monitors"; import { getAllAgents } from "../../api/agents"; import { Agent } from "../../types/agents"; import { - getNotificationConfig, - saveNotificationSettings, - createNotificationChannel, - updateNotificationChannel, - deleteNotificationChannel, - createNotificationTemplate, - updateNotificationTemplate, - deleteNotificationTemplate, - NotificationChannel as ApiNotificationChannel, - NotificationTemplate as ApiNotificationTemplate, - NotificationSettings as ApiNotificationSettings, + getNotificationConfig, + saveNotificationSettings, + createNotificationChannel, + updateNotificationChannel, + deleteNotificationChannel, + createNotificationTemplate, + updateNotificationTemplate, + deleteNotificationTemplate, + NotificationSettings as ApiNotificationSettings, } from "../../api/notifications"; +import type { + NotificationChannel as ApiNotificationChannel, + NotificationTemplate as ApiNotificationTemplate, +} from "../../types/notification"; import ChannelSelector from "../../components/ChannelSelector"; const NotificationsConfig = () => { - // 状态管理 - const [channels, setChannels] = useState([]); - const [templates, setTemplates] = useState([]); - const [settings, setSettings] = useState( - null - ); - const [monitors, setMonitors] = useState([]); - const [agents, setAgents] = useState([]); - const [loading, setLoading] = useState(true); - const [monitorsLoading, setMonitorsLoading] = useState(true); - const [agentsLoading, setAgentsLoading] = useState(true); - const [saving, setSaving] = useState(false); - - // 渠道管理状态 - const [isAddChannelOpen, setIsAddChannelOpen] = useState(false); - const [isEditChannelOpen, setIsEditChannelOpen] = useState(false); - const [isDeleteChannelOpen, setIsDeleteChannelOpen] = useState(false); - const [selectedChannelId, setSelectedChannelId] = useState( - null - ); - const [channelForm, setChannelForm] = useState({ + // 状态管理 + const [channels, setChannels] = useState([]); + const [templates, setTemplates] = useState([]); + const [settings, setSettings] = useState( + null + ); + const [monitors, setMonitors] = useState([]); + const [agents, setAgents] = useState([]); + const [loading, setLoading] = useState(true); + const [monitorsLoading, setMonitorsLoading] = useState(true); + const [agentsLoading, setAgentsLoading] = useState(true); + const [saving, setSaving] = useState(false); + + // 渠道管理状态 + const [isAddChannelOpen, setIsAddChannelOpen] = useState(false); + const [isEditChannelOpen, setIsEditChannelOpen] = useState(false); + const [isDeleteChannelOpen, setIsDeleteChannelOpen] = useState(false); + const [selectedChannelId, setSelectedChannelId] = useState( + null + ); + const [channelForm, setChannelForm] = useState({ + name: "", + type: "telegram", + config: { + botToken: "", + chatId: "", + apiKey: "", + from: "", + to: "", + webhookUrl: "", + }, + enabled: true, + }); + const [channelFormErrors, setChannelFormErrors] = useState({ + name: "", + botToken: "", + chatId: "", + apiKey: "", + from: "", + to: "", + webhookUrl: "", + }); + + // 模板管理状态 + const [isTemplateDialogOpen, setIsTemplateDialogOpen] = useState(false); + const [isDeleteTemplateOpen, setIsDeleteTemplateOpen] = useState(false); + const [selectedTemplate, setSelectedTemplate] = + useState(null); + const [templateForm, setTemplateForm] = useState({ + name: "", + type: "monitor", + subject: "", + content: "", + }); + + const contentTextAreaRef = useRef(null); + + const { t } = useTranslation(); + + // 模板变量 + const templateVariables = [ + { value: "${name}", key: "name" }, + { value: "${status}", key: "status" }, + { value: "${previous_status}", key: "previous_status" }, + { value: "${time}", key: "time" }, + { value: "${url}", key: "url" }, + { value: "${response_time}", key: "response_time" }, + { value: "${status_code}", key: "status_code" }, + { value: "${expected_status}", key: "expected_status" }, + { value: "${error}", key: "error" }, + { value: "${details}", key: "details" }, + { value: "${hostname}", key: "hostname" }, + { value: "${ip_addresses}", key: "ip_addresses" }, + { value: "${os}", key: "os" }, + ]; + + useEffect(() => { + loadData(); + loadMonitorsAndAgents(); + }, [t]); + + // 加载所有配置数据 + const loadData = async () => { + try { + setLoading(true); + const configResponse = await getNotificationConfig(); + + if (configResponse.success && configResponse.data) { + setChannels(configResponse.data.channels); + setTemplates(configResponse.data.templates); + setSettings(configResponse.data.settings); + } else { + console.error("获取通知配置失败:", configResponse.message); + toast.error(t("notifications.fetch.error")); + } + } catch (error) { + console.error("加载通知设置失败", error); + toast.error(t("notifications.fetch.error")); + } finally { + setLoading(false); + } + }; + + // 加载监控和客户端数据 + const loadMonitorsAndAgents = async () => { + setMonitorsLoading(true); + setAgentsLoading(true); + + const monitorsResponse = await getAllMonitors(); + if (monitorsResponse.success && monitorsResponse.monitors) { + setMonitors(monitorsResponse.monitors); + } else { + console.error("获取监控数据失败:", monitorsResponse.message); + } + setMonitorsLoading(false); + + const agentsResponse = await getAllAgents(); + if (agentsResponse.success && agentsResponse.agents) { + setAgents(agentsResponse.agents); + } + setAgentsLoading(false); + }; + + // 处理全局监控设置变更 + const handleMonitorSettingChange = (key: string, value: any) => { + if (!settings) return; + + setSettings({ + ...settings, + monitors: { + ...settings.monitors, + [key]: value, + }, + }); + }; + + // 处理全局客户端设置变更 + const handleAgentSettingChange = (key: string, value: any) => { + if (!settings) return; + + setSettings({ + ...settings, + agents: { + ...settings.agents, + [key]: value, + }, + }); + }; + + // 处理特定监控设置变更 + const handleSpecificMonitorSettingChange = ( + monitorId: string, + key: string, + value: any + ) => { + if (!settings) return; + + const currentSettings = settings.specificMonitors[monitorId] || { + enabled: false, + onDown: false, + onRecovery: false, + channels: [], + }; + + let updatedSettings = { + ...currentSettings, + [key]: value, + }; + + setSettings({ + ...settings, + specificMonitors: { + ...settings.specificMonitors, + [monitorId]: updatedSettings, + }, + }); + }; + + // 处理特定客户端设置变更 + const handleSpecificAgentSettingChange = ( + agentId: string, + key: string, + value: any + ) => { + if (!settings) return; + + const currentSettings = settings.specificAgents[agentId] || { + enabled: false, + onOffline: false, + onRecovery: false, + onCpuThreshold: false, + cpuThreshold: 90, + onMemoryThreshold: false, + memoryThreshold: 85, + onDiskThreshold: false, + diskThreshold: 90, + channels: [], + }; + + let updatedSettings = { + ...currentSettings, + [key]: value, + }; + + setSettings({ + ...settings, + specificAgents: { + ...settings.specificAgents, + [agentId]: updatedSettings, + }, + }); + }; + + // 保存所有设置 + const handleSave = async () => { + if (!settings) return; + + setSaving(true); + try { + const response = await saveNotificationSettings(settings); + + if (response.success) { + toast.success(t("notifications.save.success")); + } else { + toast.error(t("notifications.save.error")); + } + } catch (err) { + console.error("保存通知设置失败", err); + toast.error(t("notifications.save.error")); + } finally { + setSaving(false); + } + }; + + // 打开新增渠道对话框 + const handleAddChannelClick = () => { + setChannelForm({ name: "", type: "telegram", config: { @@ -85,160 +303,831 @@ const NotificationsConfig = () => { }, enabled: true, }); - const [channelFormErrors, setChannelFormErrors] = useState({ - name: "", - botToken: "", - chatId: "", - apiKey: "", - from: "", - to: "", - webhookUrl: "", + setChannelFormErrors({ + name: "", + botToken: "", + chatId: "", + apiKey: "", + from: "", + to: "", + webhookUrl: "", }); + setIsAddChannelOpen(true); + }; - // 模板管理状态 - const [isTemplateDialogOpen, setIsTemplateDialogOpen] = useState(false); - const [isDeleteTemplateOpen, setIsDeleteTemplateOpen] = useState(false); - const [selectedTemplate, setSelectedTemplate] = - useState(null); - const [templateForm, setTemplateForm] = useState({ - name: "", - type: "monitor", - subject: "", - content: "", + // 打开编辑渠道对话框 + const handleEditChannelClick = (channel: ApiNotificationChannel) => { + if (channel.id === undefined || channel.id === null) { + console.error("无效的渠道ID:", channel.id); + toast.error(t("notifications.channels.invalidId")); + return; + } + + setSelectedChannelId(channel.id); + + let rawConfig: unknown = channel.config; + if (typeof rawConfig === "string") { + try { + rawConfig = JSON.parse(rawConfig); + } catch (e) { + console.error(`解析渠道类型${channel.type}的配置失败:`, e); + rawConfig = {}; + } + } + + const normalizedConfig = + rawConfig && typeof rawConfig === "object" && !Array.isArray(rawConfig) + ? (rawConfig as Record) + : {}; + + const readString = (value: unknown) => + typeof value === "string" ? value : ""; + + setChannelForm({ + name: channel.name, + type: channel.type, + config: { + botToken: readString(normalizedConfig.botToken), + chatId: readString(normalizedConfig.chatId), + apiKey: readString(normalizedConfig.apiKey), + from: readString(normalizedConfig.from), + to: readString(normalizedConfig.to), + webhookUrl: readString(normalizedConfig.webhookUrl), + }, + enabled: channel.enabled, }); + setChannelFormErrors({ + name: "", + botToken: "", + chatId: "", + apiKey: "", + from: "", + to: "", + webhookUrl: "", + }); + setIsEditChannelOpen(true); + }; - const contentTextAreaRef = useRef(null); + // 打开删除渠道对话框 + const handleDeleteChannelClick = (channelId: number) => { + if (channelId === undefined || channelId === null) { + console.error("无效的渠道ID:", channelId); + toast.error(t("notifications.channels.invalidId")); + return; + } - const { t } = useTranslation(); + setSelectedChannelId(channelId); + setIsDeleteChannelOpen(true); + }; - // 模板变量 - const templateVariables = [ - { value: "${name}", key: "name" }, - { value: "${status}", key: "status" }, - { value: "${previous_status}", key: "previous_status" }, - { value: "${time}", key: "time" }, - { value: "${url}", key: "url" }, - { value: "${response_time}", key: "response_time" }, - { value: "${status_code}", key: "status_code" }, - { value: "${expected_status}", key: "expected_status" }, - { value: "${error}", key: "error" }, - { value: "${details}", key: "details" }, - { value: "${hostname}", key: "hostname" }, - { value: "${ip_addresses}", key: "ip_addresses" }, - { value: "${os}", key: "os" }, - ]; - - useEffect(() => { - loadData(); - loadMonitorsAndAgents(); - }, [t]); - - // 加载所有配置数据 - const loadData = async () => { - try { - setLoading(true); - const configResponse = await getNotificationConfig(); - - if (configResponse.success && configResponse.data) { - setChannels(configResponse.data.channels); - setTemplates(configResponse.data.templates); - setSettings(configResponse.data.settings); - } else { - console.error("获取通知配置失败:", configResponse.message); - toast.error(t("notifications.fetch.error")); - } - } catch (error) { - console.error("加载通知设置失败", error); - toast.error(t("notifications.fetch.error")); - } finally { - setLoading(false); - } + // 验证渠道表单 + const validateChannelForm = (): boolean => { + const errors = { + name: "", + botToken: "", + chatId: "", + apiKey: "", + from: "", + to: "", + webhookUrl: "", }; - // 加载监控和客户端数据 - const loadMonitorsAndAgents = async () => { - setMonitorsLoading(true); - setAgentsLoading(true); + let isValid = true; - const monitorsResponse = await getAllMonitors(); - if (monitorsResponse.success && monitorsResponse.monitors) { - setMonitors(monitorsResponse.monitors); + if (!channelForm.name.trim()) { + errors.name = t("notifications.channels.errors.nameRequired"); + isValid = false; + } + + if (channelForm.type === "telegram") { + if (!channelForm.config.botToken.trim()) { + errors.botToken = t("notifications.channels.errors.botTokenRequired"); + isValid = false; + } + if (!channelForm.config.chatId.trim()) { + errors.chatId = t("notifications.channels.errors.chatIdRequired"); + isValid = false; + } + } + + if (channelForm.type === "resend") { + if (!channelForm.config.apiKey.trim()) { + errors.apiKey = t("notifications.channels.errors.apiKeyRequired"); + isValid = false; + } + if (!channelForm.config.from.trim()) { + errors.from = t("notifications.channels.errors.fromRequired"); + isValid = false; + } + if (!channelForm.config.to.trim()) { + errors.to = t("notifications.channels.errors.toRequired"); + isValid = false; + } + } + + if (channelForm.type === "feishu" || channelForm.type === "wecom") { + if (!channelForm.config.webhookUrl.trim()) { + errors.webhookUrl = t( + "notifications.channels.errors.webhookUrlRequired" + ); + isValid = false; + } + } + + setChannelFormErrors(errors); + return isValid; + }; + + // 保存渠道 + const handleSaveChannel = async () => { + if (!validateChannelForm()) return; + + setSaving(true); + try { + const channelData = { + name: channelForm.name, + type: channelForm.type, + enabled: channelForm.enabled, + config: channelForm.config, // 直接传递对象 + }; + + if (isEditChannelOpen && selectedChannelId !== null) { + const response = await updateNotificationChannel( + selectedChannelId, + channelData + ); + if (response.success) { + toast.success(t("notifications.channels.updateSuccess")); + await loadData(); + setIsEditChannelOpen(false); } else { - console.error("获取监控数据失败:", monitorsResponse.message); + toast.error(t("notifications.channels.saveError")); } - setMonitorsLoading(false); - - const agentsResponse = await getAllAgents(); - if (agentsResponse.success && agentsResponse.agents) { - setAgents(agentsResponse.agents); + } else { + const response = await createNotificationChannel(channelData); + if (response.success) { + toast.success(t("notifications.channels.createSuccess")); + await loadData(); + setIsAddChannelOpen(false); + } else { + toast.error(t("notifications.channels.saveError")); } - setAgentsLoading(false); - }; + } + } catch (error) { + toast.error(t("notifications.channels.saveError")); + } finally { + setSaving(false); + } + }; - // 处理全局监控设置变更 - const handleMonitorSettingChange = (key: string, value: any) => { - if (!settings) return; + // 确认删除渠道 + const handleConfirmDeleteChannel = async () => { + if (selectedChannelId === null) return; + setSaving(true); + try { + const response = await deleteNotificationChannel(selectedChannelId); + if (response.success) { + toast.success(t("notifications.channels.deleteSuccess")); + await loadData(); + } else { + toast.error(t("notifications.channels.deleteError")); + } + setIsDeleteChannelOpen(false); + } catch (error) { + toast.error(t("notifications.channels.deleteError")); + } finally { + setSaving(false); + } + }; - setSettings({ - ...settings, - monitors: { - ...settings.monitors, - [key]: value, - }, + // 模板操作 + const handleAddTemplateClick = () => { + setSelectedTemplate(null); + setTemplateForm({ + name: "", + type: "monitor", + subject: "", + content: "", + }); + setIsTemplateDialogOpen(true); + }; + + const handleEditTemplateClick = (template: ApiNotificationTemplate) => { + setSelectedTemplate(template); + setTemplateForm({ + name: template.name, + type: template.type, + subject: template.subject, + content: template.content, + }); + setIsTemplateDialogOpen(true); + }; + + const handleDeleteTemplateClick = (template: ApiNotificationTemplate) => { + setSelectedTemplate(template); + setIsDeleteTemplateOpen(true); + }; + + // 保存模板 + const handleSaveTemplate = async () => { + setSaving(true); + try { + if (selectedTemplate) { + // 更新模板 + const response = await updateNotificationTemplate( + selectedTemplate.id, + templateForm + ); + if (response.success) { + toast.success(t("notifications.templates.updateSuccess")); + await loadData(); + } else { + toast.error(t("notifications.templates.updateError")); + } + } else { + // 创建新模板 + const response = await createNotificationTemplate({ + ...templateForm, + isDefault: false, }); - }; + if (response.success) { + toast.success(t("notifications.templates.createSuccess")); + await loadData(); + } else { + toast.error(t("notifications.templates.createError")); + } + } + setIsTemplateDialogOpen(false); + } catch (error) { + toast.error(t("notifications.templates.saveError")); + } finally { + setSaving(false); + } + }; - // 处理全局客户端设置变更 - const handleAgentSettingChange = (key: string, value: any) => { - if (!settings) return; + // 确认删除模板 + const handleConfirmDeleteTemplate = async () => { + if (!selectedTemplate) return; + setSaving(true); + try { + const response = await deleteNotificationTemplate(selectedTemplate.id); + if (response.success) { + toast.success(t("notifications.templates.deleteSuccess")); + await loadData(); + } else { + toast.error(t("notifications.templates.deleteError")); + } + setIsDeleteTemplateOpen(false); + } catch (error) { + toast.error(t("notifications.templates.deleteError")); + } finally { + setSaving(false); + } + }; - setSettings({ - ...settings, - agents: { - ...settings.agents, - [key]: value, - }, - }); - }; + // 插入模板变量 + const insertVariable = (variable: string) => { + if (contentTextAreaRef.current) { + const start = contentTextAreaRef.current.selectionStart; + const end = contentTextAreaRef.current.selectionEnd; + const text = contentTextAreaRef.current.value; + const newText = text.substring(0, start) + variable + text.substring(end); + setTemplateForm({ ...templateForm, content: newText }); + contentTextAreaRef.current.focus(); + } + }; - // 处理特定监控设置变更 - const handleSpecificMonitorSettingChange = ( - monitorId: string, - key: string, - value: any - ) => { - if (!settings) return; + // 渲染渠道Tab + const renderChannelsTab = () => { + if (!settings) return {t("common.loading")}...; - const currentSettings = settings.specificMonitors[monitorId] || { + return ( + + + {t("notifications.channels.tabDescription")} + + + + + {t("notifications.channels.title")} + + + + + + + {t("notifications.channels.description")} + + + {channels.length === 0 ? ( + + {t("notifications.channels.noChannels")} + + ) : ( + + {channels.map((channel) => ( + + + + + {channel.name} + + + {t(`notifications.channels.type.${channel.type}`)} + + + + + + + + + ))} + + )} + + + + + ); + }; + + // 渲染模板Tab + const renderTemplatesTab = () => { + if (!settings) return {t("common.loading")}...; + + return ( + + + {t("notifications.templates.tabDescription")} + + + + + {t("notifications.templates.title")} + + + + + + {t("notifications.templates.description")} + + + {templates.length === 0 ? ( + + {t("notifications.templates.noTemplates")} + + ) : ( + + {templates.map((template) => ( + + + + + {template.name} + {template.isDefault && ( + + {t("notifications.templates.defaultTemplate")} + + )} + + + + + + + + + {t("notifications.templates.subject")}: + + {template.subject} + + + + {t("notifications.templates.content")}: + + {template.content} + + + + ))} + + )} + + + + ); + }; + + // 渲染全局设置Tab + const renderGlobalSettingsTab = () => { + if (!settings) return {t("common.loading")}...; + + return ( + + + {t("notifications.globalSettings.description")} + + + + {t("notifications.settings.monitors")} + + + + + + + + {t("notifications.settings.monitors")} + + + {t("notifications.settings.monitors.description")} + + + + handleMonitorSettingChange("enabled", checked) + } + /> + + {settings.monitors.enabled && ( + + + + + handleMonitorSettingChange("onDown", checked) + } + /> + + {t("notifications.events.onDownOnly")} + + + + + handleMonitorSettingChange("onRecovery", checked) + } + /> + + {t("notifications.events.onRecovery")} + + + + + {t("notifications.specificSettings.channels")} + + + handleMonitorSettingChange("channels", channelIds) + } + /> + + + + )} + + + + + + + {t("notifications.settings.agents")} + + + + + + + + {t("notifications.settings.agents")} + + + {t("notifications.settings.agents.description")} + + + + handleAgentSettingChange("enabled", checked) + } + /> + + {settings.agents.enabled && ( + + + + + handleAgentSettingChange("onOffline", checked) + } + /> + + {t("notifications.events.onOffline")} + + + + + handleAgentSettingChange("onRecovery", checked) + } + /> + + {t("notifications.events.onRecoveryAgent")} + + + + + handleAgentSettingChange("onCpuThreshold", checked) + } + /> + + {t("notifications.events.onCpuThreshold")} + + + {settings.agents.onCpuThreshold && ( + + + {t("notifications.threshold.label")} + + + handleAgentSettingChange( + "cpuThreshold", + Number(e.target.value) + ) + } + /> + + {t("notifications.threshold.percent")} + + + )} + + + handleAgentSettingChange( + "onMemoryThreshold", + checked + ) + } + /> + + {t("notifications.events.onMemoryThreshold")} + + + {settings.agents.onMemoryThreshold && ( + + + {t("notifications.threshold.label")} + + + handleAgentSettingChange( + "memoryThreshold", + Number(e.target.value) + ) + } + /> + + {t("notifications.threshold.percent")} + + + )} + + + handleAgentSettingChange("onDiskThreshold", checked) + } + /> + + {t("notifications.events.onDiskThreshold")} + + + {settings.agents.onDiskThreshold && ( + + + {t("notifications.threshold.label")} + + + handleAgentSettingChange( + "diskThreshold", + Number(e.target.value) + ) + } + /> + + {t("notifications.threshold.percent")} + + + )} + + + {t("notifications.specificSettings.channels")} + + + handleAgentSettingChange("channels", channelIds) + } + /> + + + + )} + + + + + + ); + }; + + // 渲染特定监控设置Tab + const renderSpecificMonitorsTab = () => { + if (!settings) return {t("common.loading")}...; + if (monitorsLoading) return {t("common.loading")}...; + + if (monitors.length === 0) { + return {t("monitors.noMonitors")}; + } + + return ( + + + {t("notifications.specificMonitors.description")} + + + {monitors.map((monitor) => { + const monitorId = monitor.id.toString(); + const specificSettings = settings.specificMonitors[monitorId] || { enabled: false, onDown: false, onRecovery: false, channels: [], - }; + }; - let updatedSettings = { - ...currentSettings, - [key]: value, - }; + return ( + + + + + {monitor.name} + + {monitor.url} + + + + handleSpecificMonitorSettingChange( + monitorId, + "enabled", + checked + ) + } + /> + - setSettings({ - ...settings, - specificMonitors: { - ...settings.specificMonitors, - [monitorId]: updatedSettings, - }, - }); - }; + {specificSettings.enabled && ( + + + + + handleSpecificMonitorSettingChange( + monitorId, + "onDown", + checked + ) + } + /> + + {t("notifications.events.onDownOnly")} + + + + + handleSpecificMonitorSettingChange( + monitorId, + "onRecovery", + checked + ) + } + /> + + {t("notifications.events.onRecovery")} + + + + + {t("notifications.specificSettings.channels")} + + + handleSpecificMonitorSettingChange( + monitorId, + "channels", + channelIds + ) + } + /> + + + + )} + + + ); + })} + + ); + }; - // 处理特定客户端设置变更 - const handleSpecificAgentSettingChange = ( - agentId: string, - key: string, - value: any - ) => { - if (!settings) return; + // 渲染特定客户端设置Tab + const renderSpecificAgentsTab = () => { + if (!settings) return {t("common.loading")}...; + if (agentsLoading) return {t("common.loading")}...; - const currentSettings = settings.specificAgents[agentId] || { + if (agents.length === 0) { + return {t("agents.noAgents")}; + } + + return ( + + + {t("notifications.specificAgents.description")} + + + {agents.map((agent) => { + const agentId = agent.id.toString(); + const specificSettings = settings.specificAgents[agentId] || { enabled: false, onOffline: false, onRecovery: false, @@ -249,1393 +1138,626 @@ const NotificationsConfig = () => { onDiskThreshold: false, diskThreshold: 90, channels: [], - }; + }; - let updatedSettings = { - ...currentSettings, - [key]: value, - }; - - setSettings({ - ...settings, - specificAgents: { - ...settings.specificAgents, - [agentId]: updatedSettings, - }, - }); - }; - - // 保存所有设置 - const handleSave = async () => { - if (!settings) return; - - setSaving(true); - try { - const response = await saveNotificationSettings(settings); - - if (response.success) { - toast.success(t("notifications.save.success")); - } else { - toast.error(t("notifications.save.error")); - } - } catch (err) { - console.error("保存通知设置失败", err); - toast.error(t("notifications.save.error")); - } finally { - setSaving(false); - } - }; - - // 打开新增渠道对话框 - const handleAddChannelClick = () => { - setChannelForm({ - name: "", - type: "telegram", - config: { - botToken: "", - chatId: "", - apiKey: "", - from: "", - to: "", - webhookUrl: "", - }, - enabled: true, - }); - setChannelFormErrors({ - name: "", - botToken: "", - chatId: "", - apiKey: "", - from: "", - to: "", - webhookUrl: "", - }); - setIsAddChannelOpen(true); - }; - - // 打开编辑渠道对话框 - const handleEditChannelClick = (channel: ApiNotificationChannel) => { - if (!channel.id || isNaN(Number(channel.id))) { - console.error("无效的渠道ID:", channel.id); - toast.error(t("notifications.channels.invalidId")); - return; - } - - setSelectedChannelId(channel.id); - - let config = channel.config || {}; - if (typeof config === "string") { - try { - config = JSON.parse(config); - } catch (e) { - console.error(`解析渠道类型${channel.type}的配置失败:`, e); - config = {}; - } - } - - setChannelForm({ - name: channel.name, - type: channel.type, - config: { - botToken: config.botToken || "", - chatId: config.chatId || "", - apiKey: config.apiKey || "", - from: config.from || "", - to: config.to || "", - webhookUrl: config.webhookUrl || "", - }, - enabled: channel.enabled, - }); - setChannelFormErrors({ - name: "", - botToken: "", - chatId: "", - apiKey: "", - from: "", - to: "", - webhookUrl: "", - }); - setIsEditChannelOpen(true); - }; - - // 打开删除渠道对话框 - const handleDeleteChannelClick = (channelId: string) => { - if (!channelId || isNaN(Number(channelId))) { - console.error("无效的渠道ID:", channelId); - toast.error(t("notifications.channels.invalidId")); - return; - } - - setSelectedChannelId(channelId); - setIsDeleteChannelOpen(true); - }; - - // 验证渠道表单 - const validateChannelForm = (): boolean => { - const errors = { - name: "", - botToken: "", - chatId: "", - apiKey: "", - from: "", - to: "", - webhookUrl: "", - }; - - let isValid = true; - - if (!channelForm.name.trim()) { - errors.name = t("notifications.channels.errors.nameRequired"); - isValid = false; - } - - if (channelForm.type === "telegram") { - if (!channelForm.config.botToken.trim()) { - errors.botToken = t("notifications.channels.errors.botTokenRequired"); - isValid = false; - } - if (!channelForm.config.chatId.trim()) { - errors.chatId = t("notifications.channels.errors.chatIdRequired"); - isValid = false; - } - } - - if (channelForm.type === "resend") { - if (!channelForm.config.apiKey.trim()) { - errors.apiKey = t("notifications.channels.errors.apiKeyRequired"); - isValid = false; - } - if (!channelForm.config.from.trim()) { - errors.from = t("notifications.channels.errors.fromRequired"); - isValid = false; - } - if (!channelForm.config.to.trim()) { - errors.to = t("notifications.channels.errors.toRequired"); - isValid = false; - } - } - - if (channelForm.type === "feishu" || channelForm.type === "wecom") { - if (!channelForm.config.webhookUrl.trim()) { - errors.webhookUrl = t("notifications.channels.errors.webhookUrlRequired"); - isValid = false; - } - } - - setChannelFormErrors(errors); - return isValid; - }; - - // 保存渠道 - const handleSaveChannel = async () => { - if (!validateChannelForm()) return; - - setSaving(true); - try { - const channelData = { - name: channelForm.name, - type: channelForm.type, - enabled: channelForm.enabled, - config: channelForm.config, // 直接传递对象 - }; - - if (isEditChannelOpen && selectedChannelId) { - const response = await updateNotificationChannel(selectedChannelId, channelData); - if (response.success) { - toast.success(t("notifications.channels.updateSuccess")); - await loadData(); - setIsEditChannelOpen(false); - } else { - toast.error(t("notifications.channels.saveError")); - } - } else { - const response = await createNotificationChannel(channelData); - if (response.success) { - toast.success(t("notifications.channels.createSuccess")); - await loadData(); - setIsAddChannelOpen(false); - } else { - toast.error(t("notifications.channels.saveError")); - } - } - } catch (error) { - toast.error(t("notifications.channels.saveError")); - } finally { - setSaving(false); - } - }; - - // 确认删除渠道 - const handleConfirmDeleteChannel = async () => { - if (!selectedChannelId) return; - setSaving(true); - try { - const response = await deleteNotificationChannel(selectedChannelId); - if (response.success) { - toast.success(t("notifications.channels.deleteSuccess")); - await loadData(); - } else { - toast.error(t("notifications.channels.deleteError")); - } - setIsDeleteChannelOpen(false); - } catch (error) { - toast.error(t("notifications.channels.deleteError")); - } finally { - setSaving(false); - } - }; - - - // 模板操作 - const handleAddTemplateClick = () => { - setSelectedTemplate(null); - setTemplateForm({ - name: "", - type: "monitor", - subject: "", - content: "", - }); - setIsTemplateDialogOpen(true); - }; - - const handleEditTemplateClick = (template: ApiNotificationTemplate) => { - setSelectedTemplate(template); - setTemplateForm({ - name: template.name, - type: template.type, - subject: template.subject, - content: template.content, - }); - setIsTemplateDialogOpen(true); - }; - - const handleDeleteTemplateClick = (template: ApiNotificationTemplate) => { - setSelectedTemplate(template); - setIsDeleteTemplateOpen(true); - }; - - // 保存模板 - const handleSaveTemplate = async () => { - setSaving(true); - try { - if (selectedTemplate) { - // 更新模板 - const response = await updateNotificationTemplate( - selectedTemplate.id, - templateForm - ); - if (response.success) { - toast.success(t("notifications.templates.updateSuccess")); - await loadData(); - } else { - toast.error(t("notifications.templates.updateError")); - } - } else { - // 创建新模板 - const response = await createNotificationTemplate({ ...templateForm, isDefault: false }); - if (response.success) { - toast.success(t("notifications.templates.createSuccess")); - await loadData(); - } else { - toast.error(t("notifications.templates.createError")); - } - } - setIsTemplateDialogOpen(false); - } catch (error) { - toast.error(t("notifications.templates.saveError")); - } finally { - setSaving(false); - } - }; - - // 确认删除模板 - const handleConfirmDeleteTemplate = async () => { - if (!selectedTemplate) return; - setSaving(true); - try { - const response = await deleteNotificationTemplate(selectedTemplate.id); - if (response.success) { - toast.success(t("notifications.templates.deleteSuccess")); - await loadData(); - } else { - toast.error(t("notifications.templates.deleteError")); - } - setIsDeleteTemplateOpen(false); - } catch (error) { - toast.error(t("notifications.templates.deleteError")); - } finally { - setSaving(false); - } - }; - - // 插入模板变量 - const insertVariable = (variable: string) => { - if (contentTextAreaRef.current) { - const start = contentTextAreaRef.current.selectionStart; - const end = contentTextAreaRef.current.selectionEnd; - const text = contentTextAreaRef.current.value; - const newText = - text.substring(0, start) + variable + text.substring(end); - setTemplateForm({ ...templateForm, content: newText }); - contentTextAreaRef.current.focus(); - } - }; - - // 渲染渠道Tab - const renderChannelsTab = () => { - if (!settings) return {t("common.loading")}...; - - return ( - - - {t("notifications.channels.tabDescription")} - - - - - {t("notifications.channels.title")} - - - - - - - {t("notifications.channels.description")} - - - {channels.length === 0 ? ( - - {t("notifications.channels.noChannels")} - - ) : ( - - {channels.map((channel) => ( - - - - - {channel.name} - - - {t(`notifications.channels.type.${channel.type}`)} - - - - - - - - - ))} - - )} - - - - - ); - }; - - // 渲染模板Tab - const renderTemplatesTab = () => { - if (!settings) return {t("common.loading")}...; - - return ( - - - {t("notifications.templates.tabDescription")} - - - - - {t("notifications.templates.title")} - - - - - - {t("notifications.templates.description")} - - - {templates.length === 0 ? ( - {t("notifications.templates.noTemplates")} - ) : ( - - {templates.map((template) => ( - - - - - {template.name} - {template.isDefault && ( - - {t("notifications.templates.defaultTemplate")} - - )} - - - - - - - - - {t("notifications.templates.subject")}: - - {template.subject} - - - - {t("notifications.templates.content")}: - - {template.content} - - - - ))} - - )} - - - - ); - }; - - // 渲染全局设置Tab - const renderGlobalSettingsTab = () => { - if (!settings) return {t("common.loading")}...; - - return ( - - - {t("notifications.globalSettings.description")} - - - - {t("notifications.settings.monitors")} + return ( + + + + + {agent.name} + + {(() => { + try { + const ipArray = JSON.parse( + String(agent.ip_addresses || "[]") + ); + return Array.isArray(ipArray) && ipArray.length > 0 + ? ipArray.join(", ") + : String(agent.ip_addresses || ""); + } catch (e) { + return String(agent.ip_addresses || ""); + } + })()} - - - - - - - {t("notifications.settings.monitors")} - - - {t("notifications.settings.monitors.description")} - - - - handleMonitorSettingChange("enabled", checked) - } - /> - - {settings.monitors.enabled && ( - - - - - handleMonitorSettingChange("onDown", checked) - } - /> - - {t("notifications.events.onDownOnly")} - - - - - handleMonitorSettingChange("onRecovery", checked) - } - /> - - {t("notifications.events.onRecovery")} - - - - - {t("notifications.specificSettings.channels")} - - - handleMonitorSettingChange("channels", channelIds) - } - /> - - - - )} - - - - - - - {t("notifications.settings.agents")} - - - - - - - - {t("notifications.settings.agents")} - - - {t("notifications.settings.agents.description")} - - - - handleAgentSettingChange("enabled", checked) - } - /> - - {settings.agents.enabled && ( - - - - - handleAgentSettingChange("onOffline", checked) - } - /> - - {t("notifications.events.onOffline")} - - - - - handleAgentSettingChange("onRecovery", checked) - } - /> - - {t("notifications.events.onRecoveryAgent")} - - - - - handleAgentSettingChange("onCpuThreshold", checked) - } - /> - - {t("notifications.events.onCpuThreshold")} - - - {settings.agents.onCpuThreshold && ( - - - {t("notifications.threshold.label")} - - - handleAgentSettingChange( - "cpuThreshold", - Number(e.target.value) - ) - } - /> - - {t("notifications.threshold.percent")} - - - )} - - - handleAgentSettingChange( - "onMemoryThreshold", - checked - ) - } - /> - - {t("notifications.events.onMemoryThreshold")} - - - {settings.agents.onMemoryThreshold && ( - - - {t("notifications.threshold.label")} - - - handleAgentSettingChange( - "memoryThreshold", - Number(e.target.value) - ) - } - /> - - {t("notifications.threshold.percent")} - - - )} - - - handleAgentSettingChange("onDiskThreshold", checked) - } - /> - - {t("notifications.events.onDiskThreshold")} - - - {settings.agents.onDiskThreshold && ( - - - {t("notifications.threshold.label")} - - - handleAgentSettingChange( - "diskThreshold", - Number(e.target.value) - ) - } - /> - - {t("notifications.threshold.percent")} - - - )} - - - {t("notifications.specificSettings.channels")} - - - handleAgentSettingChange("channels", channelIds) - } - /> - - - - )} - - - - - - ); - }; - - // 渲染特定监控设置Tab - const renderSpecificMonitorsTab = () => { - if (!settings) return {t("common.loading")}...; - if (monitorsLoading) return {t("common.loading")}...; - - if (monitors.length === 0) { - return {t("monitors.noMonitors")}; - } - - return ( - - - {t("notifications.specificMonitors.description")} - - - {monitors.map((monitor) => { - const monitorId = monitor.id.toString(); - const specificSettings = settings.specificMonitors[monitorId] || { - enabled: false, - onDown: false, - onRecovery: false, - channels: [], - }; - - return ( - - - - - {monitor.name} - - {monitor.url} - - - - handleSpecificMonitorSettingChange( - monitorId, - "enabled", - checked - ) - } - /> - - - {specificSettings.enabled && ( - - - - - handleSpecificMonitorSettingChange( - monitorId, - "onDown", - checked - ) - } - /> - - {t("notifications.events.onDownOnly")} - - - - - handleSpecificMonitorSettingChange( - monitorId, - "onRecovery", - checked - ) - } - /> - - {t("notifications.events.onRecovery")} - - - - - {t("notifications.specificSettings.channels")} - - - handleSpecificMonitorSettingChange( - monitorId, - "channels", - channelIds - ) - } - /> - - - - )} - - - ); - })} - - ); - }; - - // 渲染特定客户端设置Tab - const renderSpecificAgentsTab = () => { - if (!settings) return {t("common.loading")}...; - if (agentsLoading) return {t("common.loading")}...; - - if (agents.length === 0) { - return {t("agents.noAgents")}; - } - - return ( - - - {t("notifications.specificAgents.description")} - - - {agents.map((agent) => { - const agentId = agent.id.toString(); - const specificSettings = settings.specificAgents[agentId] || { - enabled: false, - onOffline: false, - onRecovery: false, - onCpuThreshold: false, - cpuThreshold: 90, - onMemoryThreshold: false, - memoryThreshold: 85, - onDiskThreshold: false, - diskThreshold: 90, - channels: [], - }; - - return ( - - - - - {agent.name} - - {(() => { - try { - const ipArray = JSON.parse( - String(agent.ip_addresses || "[]") - ); - return Array.isArray(ipArray) && ipArray.length > 0 - ? ipArray.join(", ") - : String(agent.ip_addresses || ""); - } catch (e) { - return String(agent.ip_addresses || ""); - } - })()} - - - - handleSpecificAgentSettingChange( - agentId, - "enabled", - checked - ) - } - /> - - {specificSettings.enabled && ( - - - - - handleSpecificAgentSettingChange( - agentId, - "onOffline", - checked - ) - } - /> - - {t("notifications.events.onOffline")} - - - - - handleSpecificAgentSettingChange( - agentId, - "onRecovery", - checked - ) - } - /> - - {t("notifications.events.onRecoveryAgent")} - - - - - handleSpecificAgentSettingChange( - agentId, - "onCpuThreshold", - checked - ) - } - /> - - {t("notifications.events.onCpuThreshold")} - - - {specificSettings.onCpuThreshold && ( - - - {t("notifications.threshold.label")} - - - handleSpecificAgentSettingChange( - agentId, - "cpuThreshold", - Number(e.target.value) - ) - } - /> - - {t("notifications.threshold.percent")} - - - )} - - - handleSpecificAgentSettingChange( - agentId, - "onMemoryThreshold", - checked - ) - } - /> - - {t("notifications.events.onMemoryThreshold")} - - - {specificSettings.onMemoryThreshold && ( - - - {t("notifications.threshold.label")} - - - handleSpecificAgentSettingChange( - agentId, - "memoryThreshold", - Number(e.target.value) - ) - } - /> - - {t("notifications.threshold.percent")} - - - )} - - - handleSpecificAgentSettingChange( - agentId, - "onDiskThreshold", - checked - ) - } - /> - - {t("notifications.events.onDiskThreshold")} - - - {specificSettings.onDiskThreshold && ( - - - {t("notifications.threshold.label")} - - - handleSpecificAgentSettingChange( - agentId, - "diskThreshold", - Number(e.target.value) - ) - } - /> - - {t("notifications.threshold.percent")} - - - )} - - - {t("notifications.specificSettings.channels")} - - - handleSpecificAgentSettingChange( - agentId, - "channels", - channelIds - ) - } - /> - - - - )} - - - ); - })} - - ); - }; - - // 渲染渠道对话框 - const renderChannelDialog = () => { - const isOpen = isAddChannelOpen || isEditChannelOpen; - const title = isEditChannelOpen ? t("notifications.channels.edit") : t("notifications.channels.add"); - - return ( - { - if (!open) { - setIsAddChannelOpen(false); - setIsEditChannelOpen(false); - } - }}> - - {title} - {t("notifications.channels.dialogDescription")} - - setChannelForm({ ...channelForm, name: e.target.value })} - /> - {channelFormErrors.name && {channelFormErrors.name}} - - {channelForm.type === 'telegram' && ( - <> - setChannelForm({ ...channelForm, config: { ...channelForm.config, botToken: e.target.value } })} - /> - {channelFormErrors.botToken && {channelFormErrors.botToken}} - setChannelForm({ ...channelForm, config: { ...channelForm.config, chatId: e.target.value } })} - /> - {channelFormErrors.chatId && {channelFormErrors.chatId}} - - )} - {channelForm.type === 'resend' && ( - <> - setChannelForm({ ...channelForm, config: { ...channelForm.config, apiKey: e.target.value } })} - /> - {channelFormErrors.apiKey && {channelFormErrors.apiKey}} - setChannelForm({ ...channelForm, config: { ...channelForm.config, from: e.target.value } })} - /> - {channelFormErrors.from && {channelFormErrors.from}} - setChannelForm({ ...channelForm, config: { ...channelForm.config, to: e.target.value } })} - /> - {channelFormErrors.to && {channelFormErrors.to}} - - )} - {(channelForm.type === 'feishu' || channelForm.type === 'wecom') && ( - <> - setChannelForm({ ...channelForm, config: { ...channelForm.config, webhookUrl: e.target.value } })} - /> - {channelFormErrors.webhookUrl && {channelFormErrors.webhookUrl}} - - )} - - - - - - - - ) - }; - - // 渲染删除渠道对话框 - const renderDeleteChannelDialog = () => ( - - - {t("notifications.channels.deleteConfirmTitle")} - {t("notifications.channels.deleteConfirmMessage")} - - - + + + handleSpecificAgentSettingChange( + agentId, + "enabled", + checked + ) + } + /> - - + {specificSettings.enabled && ( + + + + + handleSpecificAgentSettingChange( + agentId, + "onOffline", + checked + ) + } + /> + + {t("notifications.events.onOffline")} + + + + + handleSpecificAgentSettingChange( + agentId, + "onRecovery", + checked + ) + } + /> + + {t("notifications.events.onRecoveryAgent")} + + + + + handleSpecificAgentSettingChange( + agentId, + "onCpuThreshold", + checked + ) + } + /> + + {t("notifications.events.onCpuThreshold")} + + + {specificSettings.onCpuThreshold && ( + + + {t("notifications.threshold.label")} + + + handleSpecificAgentSettingChange( + agentId, + "cpuThreshold", + Number(e.target.value) + ) + } + /> + + {t("notifications.threshold.percent")} + + + )} + + + handleSpecificAgentSettingChange( + agentId, + "onMemoryThreshold", + checked + ) + } + /> + + {t("notifications.events.onMemoryThreshold")} + + + {specificSettings.onMemoryThreshold && ( + + + {t("notifications.threshold.label")} + + + handleSpecificAgentSettingChange( + agentId, + "memoryThreshold", + Number(e.target.value) + ) + } + /> + + {t("notifications.threshold.percent")} + + + )} + + + handleSpecificAgentSettingChange( + agentId, + "onDiskThreshold", + checked + ) + } + /> + + {t("notifications.events.onDiskThreshold")} + + + {specificSettings.onDiskThreshold && ( + + + {t("notifications.threshold.label")} + + + handleSpecificAgentSettingChange( + agentId, + "diskThreshold", + Number(e.target.value) + ) + } + /> + + {t("notifications.threshold.percent")} + + + )} + + + {t("notifications.specificSettings.channels")} + + + handleSpecificAgentSettingChange( + agentId, + "channels", + channelIds + ) + } + /> + + + + )} + + + ); + })} + ); + }; - // 渲染模板对话框 - const renderTemplateDialog = () => { - return ( - - - - {selectedTemplate - ? t("notifications.templates.edit") - : t("notifications.templates.add")} - - - - setTemplateForm({ ...templateForm, name: e.target.value }) - } - /> - - - setTemplateForm({ ...templateForm, subject: e.target.value }) - } - /> -