Procházet zdrojové kódy

feat(memory): add memory threshold alerts (#5366)

* feat(memory): add memory threshold alerts

Add memory (RAM) threshold alerts following the same architecture as
CPU alerts: CheckMemJob with @every 1m cadence, memoryAlarmWanted gate,
tgMemory/smtpMemory per-subscriber settings (default 80%), EventBusCheckboxes
with inline threshold input, i18n for en-US/ru-RU with English defaults.

# Conflicts:
#	internal/web/translation/ar-EG.json
#	internal/web/translation/es-ES.json
#	internal/web/translation/fa-IR.json
#	internal/web/translation/id-ID.json
#	internal/web/translation/ja-JP.json
#	internal/web/translation/pt-BR.json
#	internal/web/translation/ru-RU.json
#	internal/web/translation/tr-TR.json
#	internal/web/translation/uk-UA.json
#	internal/web/translation/vi-VN.json
#	internal/web/translation/zh-CN.json
#	internal/web/translation/zh-TW.json

* fix: address code review findings for memory alerts

- Remove dead settingService field from CheckMemJob
- Fix cpuThreshold double-emoji in 12 locale files (code prepends 🔴)
- Align TgCpu/TgMemory fields in entity.go
- Add missing SetTgMemory function

* fix: restore settingService in CheckMemJob for consistency with CheckCpuJob
Sentiago před 19 hodinami
rodič
revize
891d3a8759

+ 28 - 0
frontend/public/openapi.json

@@ -160,6 +160,12 @@
             "description": "SMTP server host",
             "type": "string"
           },
+          "smtpMemory": {
+            "description": "Memory threshold for email notifications",
+            "maximum": 100,
+            "minimum": 0,
+            "type": "integer"
+          },
           "smtpPassword": {
             "description": "SMTP password",
             "type": "string"
@@ -335,6 +341,12 @@
             "description": "Telegram bot language",
             "type": "string"
           },
+          "tgMemory": {
+            "description": "Memory usage threshold for alerts (percent)",
+            "maximum": 100,
+            "minimum": 0,
+            "type": "integer"
+          },
           "tgRunTime": {
             "description": "Cron schedule for Telegram notifications",
             "type": "string"
@@ -428,6 +440,7 @@
           "smtpEnabledEvents",
           "smtpEncryptionType",
           "smtpHost",
+          "smtpMemory",
           "smtpPassword",
           "smtpPort",
           "smtpTo",
@@ -470,6 +483,7 @@
           "tgCpu",
           "tgEnabledEvents",
           "tgLang",
+          "tgMemory",
           "tgRunTime",
           "timeLocation",
           "trafficDiff",
@@ -641,6 +655,12 @@
             "description": "SMTP server host",
             "type": "string"
           },
+          "smtpMemory": {
+            "description": "Memory threshold for email notifications",
+            "maximum": 100,
+            "minimum": 0,
+            "type": "integer"
+          },
           "smtpPassword": {
             "description": "SMTP password",
             "type": "string"
@@ -816,6 +836,12 @@
             "description": "Telegram bot language",
             "type": "string"
           },
+          "tgMemory": {
+            "description": "Memory usage threshold for alerts (percent)",
+            "maximum": 100,
+            "minimum": 0,
+            "type": "integer"
+          },
           "tgRunTime": {
             "description": "Cron schedule for Telegram notifications",
             "type": "string"
@@ -916,6 +942,7 @@
           "smtpEnabledEvents",
           "smtpEncryptionType",
           "smtpHost",
+          "smtpMemory",
           "smtpPassword",
           "smtpPort",
           "smtpTo",
@@ -958,6 +985,7 @@
           "tgCpu",
           "tgEnabledEvents",
           "tgLang",
+          "tgMemory",
           "tgRunTime",
           "timeLocation",
           "trafficDiff",

+ 8 - 0
frontend/src/components/ui/notifications/EmailNotifications.tsx

@@ -41,6 +41,14 @@ const GROUPS: NotificationGroupConfig[] = [
           <InputNumber size="small" min={0} max={100} value={value} onChange={onChange} style={{ width: 80 }} />
         ),
       },
+      {
+        key: 'memory.high',
+        label: 'eventMemoryHigh',
+        settingKey: 'smtpMemory',
+        extra: ({ value, onChange }) => (
+          <InputNumber size="small" min={0} max={100} value={value} onChange={onChange} style={{ width: 80 }} />
+        ),
+      },
     ],
   },
   {

+ 8 - 0
frontend/src/components/ui/notifications/TelegramNotifications.tsx

@@ -41,6 +41,14 @@ const GROUPS: NotificationGroupConfig[] = [
           <InputNumber size="small" min={0} max={100} value={value} onChange={onChange} style={{ width: 80 }} />
         ),
       },
+      {
+        key: 'memory.high',
+        label: 'eventMemoryHigh',
+        settingKey: 'tgMemory',
+        extra: ({ value, onChange }) => (
+          <InputNumber size="small" min={0} max={100} value={value} onChange={onChange} style={{ width: 80 }} />
+        ),
+      },
     ],
   },
   {

+ 4 - 0
frontend/src/generated/examples.ts

@@ -35,6 +35,7 @@ export const EXAMPLES: Record<string, unknown> = {
     "smtpEnabledEvents": "",
     "smtpEncryptionType": "",
     "smtpHost": "",
+    "smtpMemory": 0,
     "smtpPassword": "",
     "smtpPort": 1,
     "smtpTo": "",
@@ -77,6 +78,7 @@ export const EXAMPLES: Record<string, unknown> = {
     "tgCpu": 0,
     "tgEnabledEvents": "",
     "tgLang": "",
+    "tgMemory": 0,
     "tgRunTime": "",
     "timeLocation": "",
     "trafficDiff": 0,
@@ -133,6 +135,7 @@ export const EXAMPLES: Record<string, unknown> = {
     "smtpEnabledEvents": "",
     "smtpEncryptionType": "",
     "smtpHost": "",
+    "smtpMemory": 0,
     "smtpPassword": "",
     "smtpPort": 1,
     "smtpTo": "",
@@ -175,6 +178,7 @@ export const EXAMPLES: Record<string, unknown> = {
     "tgCpu": 0,
     "tgEnabledEvents": "",
     "tgLang": "",
+    "tgMemory": 0,
     "tgRunTime": "",
     "timeLocation": "",
     "trafficDiff": 0,

+ 28 - 0
frontend/src/generated/schemas.ts

@@ -134,6 +134,12 @@ export const SCHEMAS: Record<string, unknown> = {
         "description": "SMTP server host",
         "type": "string"
       },
+      "smtpMemory": {
+        "description": "Memory threshold for email notifications",
+        "maximum": 100,
+        "minimum": 0,
+        "type": "integer"
+      },
       "smtpPassword": {
         "description": "SMTP password",
         "type": "string"
@@ -309,6 +315,12 @@ export const SCHEMAS: Record<string, unknown> = {
         "description": "Telegram bot language",
         "type": "string"
       },
+      "tgMemory": {
+        "description": "Memory usage threshold for alerts (percent)",
+        "maximum": 100,
+        "minimum": 0,
+        "type": "integer"
+      },
       "tgRunTime": {
         "description": "Cron schedule for Telegram notifications",
         "type": "string"
@@ -402,6 +414,7 @@ export const SCHEMAS: Record<string, unknown> = {
       "smtpEnabledEvents",
       "smtpEncryptionType",
       "smtpHost",
+      "smtpMemory",
       "smtpPassword",
       "smtpPort",
       "smtpTo",
@@ -444,6 +457,7 @@ export const SCHEMAS: Record<string, unknown> = {
       "tgCpu",
       "tgEnabledEvents",
       "tgLang",
+      "tgMemory",
       "tgRunTime",
       "timeLocation",
       "trafficDiff",
@@ -615,6 +629,12 @@ export const SCHEMAS: Record<string, unknown> = {
         "description": "SMTP server host",
         "type": "string"
       },
+      "smtpMemory": {
+        "description": "Memory threshold for email notifications",
+        "maximum": 100,
+        "minimum": 0,
+        "type": "integer"
+      },
       "smtpPassword": {
         "description": "SMTP password",
         "type": "string"
@@ -790,6 +810,12 @@ export const SCHEMAS: Record<string, unknown> = {
         "description": "Telegram bot language",
         "type": "string"
       },
+      "tgMemory": {
+        "description": "Memory usage threshold for alerts (percent)",
+        "maximum": 100,
+        "minimum": 0,
+        "type": "integer"
+      },
       "tgRunTime": {
         "description": "Cron schedule for Telegram notifications",
         "type": "string"
@@ -890,6 +916,7 @@ export const SCHEMAS: Record<string, unknown> = {
       "smtpEnabledEvents",
       "smtpEncryptionType",
       "smtpHost",
+      "smtpMemory",
       "smtpPassword",
       "smtpPort",
       "smtpTo",
@@ -932,6 +959,7 @@ export const SCHEMAS: Record<string, unknown> = {
       "tgCpu",
       "tgEnabledEvents",
       "tgLang",
+      "tgMemory",
       "tgRunTime",
       "timeLocation",
       "trafficDiff",

+ 4 - 0
frontend/src/generated/types.ts

@@ -41,6 +41,7 @@ export interface AllSetting {
   smtpEnabledEvents: string;
   smtpEncryptionType: string;
   smtpHost: string;
+  smtpMemory: number;
   smtpPassword: string;
   smtpPort: number;
   smtpTo: string;
@@ -83,6 +84,7 @@ export interface AllSetting {
   tgCpu: number;
   tgEnabledEvents: string;
   tgLang: string;
+  tgMemory: number;
   tgRunTime: string;
   timeLocation: string;
   trafficDiff: number;
@@ -140,6 +142,7 @@ export interface AllSettingView {
   smtpEnabledEvents: string;
   smtpEncryptionType: string;
   smtpHost: string;
+  smtpMemory: number;
   smtpPassword: string;
   smtpPort: number;
   smtpTo: string;
@@ -182,6 +185,7 @@ export interface AllSettingView {
   tgCpu: number;
   tgEnabledEvents: string;
   tgLang: string;
+  tgMemory: number;
   tgRunTime: string;
   timeLocation: string;
   trafficDiff: number;

+ 4 - 0
frontend/src/generated/zod.ts

@@ -53,6 +53,7 @@ export const AllSettingSchema = z.object({
   smtpEnabledEvents: z.string(),
   smtpEncryptionType: z.string(),
   smtpHost: z.string(),
+  smtpMemory: z.number().int().min(0).max(100),
   smtpPassword: z.string(),
   smtpPort: z.number().int().min(1).max(65535),
   smtpTo: z.string(),
@@ -95,6 +96,7 @@ export const AllSettingSchema = z.object({
   tgCpu: z.number().int().min(0).max(100),
   tgEnabledEvents: z.string(),
   tgLang: z.string(),
+  tgMemory: z.number().int().min(0).max(100),
   tgRunTime: z.string(),
   timeLocation: z.string(),
   trafficDiff: z.number().int().min(0).max(100),
@@ -153,6 +155,7 @@ export const AllSettingViewSchema = z.object({
   smtpEnabledEvents: z.string(),
   smtpEncryptionType: z.string(),
   smtpHost: z.string(),
+  smtpMemory: z.number().int().min(0).max(100),
   smtpPassword: z.string(),
   smtpPort: z.number().int().min(1).max(65535),
   smtpTo: z.string(),
@@ -195,6 +198,7 @@ export const AllSettingViewSchema = z.object({
   tgCpu: z.number().int().min(0).max(100),
   tgEnabledEvents: z.string(),
   tgLang: z.string(),
+  tgMemory: z.number().int().min(0).max(100),
   tgRunTime: z.string(),
   timeLocation: z.string(),
   trafficDiff: z.number().int().min(0).max(100),

+ 2 - 0
frontend/src/models/setting.ts

@@ -22,6 +22,7 @@ export class AllSetting {
   tgRunTime = '@daily';
   tgBotBackup = false;
   tgCpu = 80;
+  tgMemory = 80;
   tgLang = 'en-US';
   twoFactorEnable = false;
   twoFactorToken = '';
@@ -91,6 +92,7 @@ export class AllSetting {
   smtpEncryptionType = 'starttls';
   smtpEnabledEvents = '';
   smtpCpu = 80;
+  smtpMemory = 80;
   hasTgBotToken = false;
   hasTwoFactorToken = false;
   hasLdapPassword = false;

+ 2 - 1
internal/eventbus/events.go

@@ -18,7 +18,8 @@ const (
 	EventNodeUp   EventType = "node.up"
 
 	// System health
-	EventCPUHigh EventType = "cpu.high"
+	EventCPUHigh    EventType = "cpu.high"
+	EventMemoryHigh EventType = "memory.high"
 
 	// Security
 	EventLoginAttempt EventType = "login.attempt"

+ 3 - 1
internal/web/entity/entity.go

@@ -47,6 +47,7 @@ type AllSetting struct {
 	TgRunTime       string `json:"tgRunTime" form:"tgRunTime"`                  // Cron schedule for Telegram notifications
 	TgBotBackup     bool   `json:"tgBotBackup" form:"tgBotBackup"`              // Enable database backup via Telegram
 	TgCpu           int    `json:"tgCpu" form:"tgCpu" validate:"gte=0,lte=100"` // CPU usage threshold for alerts (percent)
+	TgMemory        int    `json:"tgMemory" form:"tgMemory" validate:"gte=0,lte=100"` // Memory usage threshold for alerts (percent)
 	TgLang          string `json:"tgLang" form:"tgLang"`                        // Telegram bot language
 	TgEnabledEvents string `json:"tgEnabledEvents" form:"tgEnabledEvents"`      // Comma-separated event types to send via Telegram
 
@@ -59,7 +60,8 @@ type AllSetting struct {
 	SmtpTo             string `json:"smtpTo" form:"smtpTo"`                                // Comma-separated recipient emails
 	SmtpEncryptionType string `json:"smtpEncryptionType" form:"smtpEncryptionType"`        // SMTP encryption: none, starttls, tls
 	SmtpEnabledEvents  string `json:"smtpEnabledEvents" form:"smtpEnabledEvents"`          // Comma-separated event types to send via email
-	SmtpCpu            int    `json:"smtpCpu" form:"smtpCpu" validate:"gte=0,lte=100"`     // CPU threshold for email notifications
+	SmtpCpu           int    `json:"smtpCpu" form:"smtpCpu" validate:"gte=0,lte=100"`                                          // CPU threshold for email notifications
+	SmtpMemory        int    `json:"smtpMemory" form:"smtpMemory" validate:"gte=0,lte=100"`                                    // Memory threshold for email notifications
 
 	// Security settings
 	TimeLocation    string `json:"timeLocation" form:"timeLocation"`       // Time zone location

+ 35 - 0
internal/web/job/check_memory_usage.go

@@ -0,0 +1,35 @@
+package job
+
+import (
+	"github.com/mhsanaei/3x-ui/v3/internal/eventbus"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/service"
+
+	"github.com/shirou/gopsutil/v4/mem"
+)
+
+// CheckMemJob monitors memory usage and publishes events when threshold is exceeded.
+type CheckMemJob struct {
+	settingService service.SettingService
+}
+
+// NewCheckMemJob creates a new memory monitoring job instance.
+func NewCheckMemJob() *CheckMemJob {
+	return new(CheckMemJob)
+}
+
+// Run checks memory usage and publishes a memory.high event with raw metric data.
+func (j *CheckMemJob) Run() {
+	memInfo, err := mem.VirtualMemory()
+	if err != nil || memInfo == nil {
+		return
+	}
+
+	if EventBus != nil {
+		EventBus.Publish(eventbus.Event{
+			Type: eventbus.EventMemoryHigh,
+			Data: &eventbus.SystemMetricData{
+				Percent: memInfo.UsedPercent,
+			},
+		})
+	}
+}

+ 13 - 0
internal/web/service/email/subscriber.go

@@ -148,6 +148,19 @@ func (s *Subscriber) formatMessage(e eventbus.Event) (subject, body string) {
 			body = wrap(subject, content)
 		}
 
+	case eventbus.EventMemoryHigh:
+		if data, ok := e.Data.(*eventbus.SystemMetricData); ok {
+			smtpMemory, err := s.settingService.GetSmtpMemory()
+			if err != nil || smtpMemory <= 0 || data.Percent <= float64(smtpMemory) {
+				return
+			}
+			subject = host + " " + i18n("tgbot.messages.memoryThreshold",
+				"Percent=="+strconv.FormatFloat(data.Percent, 'f', 2, 64),
+				"Threshold=="+fmt.Sprintf("%d", smtpMemory))
+			content := kv(i18n("email.labelStatus"), `<span style="color:orange">`+i18n("email.statusHigh")+`</span>`)
+			body = wrap(subject, content)
+		}
+
 	case eventbus.EventLoginAttempt:
 		if data, ok := e.Data.(*eventbus.LoginEventData); ok {
 			if data.Status == "success" {

+ 18 - 0
internal/web/service/setting.go

@@ -63,6 +63,7 @@ var defaultValueMap = map[string]string{
 	"tgRunTime":                   "@daily",
 	"tgBotBackup":                 "false",
 	"tgCpu":                       "80",
+	"tgMemory":                    "80",
 	"tgLang":                      "en-US",
 	"twoFactorEnable":             "false",
 	"twoFactorToken":              "",
@@ -131,6 +132,7 @@ var defaultValueMap = map[string]string{
 	"tgEnabledEvents":   "login.attempt,cpu.high",
 	"smtpEnabledEvents": "login.attempt,cpu.high",
 	"smtpCpu":           "80",
+	"smtpMemory":        "80",
 
 	// Email (SMTP) notifications
 	"smtpEnable":         "false",
@@ -531,6 +533,14 @@ func (s *SettingService) GetTgCpu() (int, error) {
 	return s.getInt("tgCpu")
 }
 
+func (s *SettingService) GetTgMemory() (int, error) {
+	return s.getInt("tgMemory")
+}
+
+func (s *SettingService) SetTgMemory(value int) error {
+	return s.setInt("tgMemory", value)
+}
+
 func (s *SettingService) GetTgLang() (string, error) {
 	return s.getString("tgLang")
 }
@@ -1017,6 +1027,14 @@ func (s *SettingService) SetSmtpCpu(value int) error {
 	return s.setInt("smtpCpu", value)
 }
 
+func (s *SettingService) GetSmtpMemory() (int, error) {
+	return s.getInt("smtpMemory")
+}
+
+func (s *SettingService) SetSmtpMemory(value int) error {
+	return s.setInt("smtpMemory", value)
+}
+
 func (s *SettingService) UpdateAllSetting(allSetting *entity.AllSetting) error {
 	if err := s.preserveRedactedSecrets(allSetting); err != nil {
 		return err

+ 12 - 0
internal/web/service/tgbot/tgbot_event.go

@@ -123,6 +123,18 @@ func (t *Tgbot) formatEventMessage(e eventbus.Event) string {
 		}
 		return ""
 
+	case eventbus.EventMemoryHigh:
+		if data, ok := e.Data.(*eventbus.SystemMetricData); ok {
+			tgMemory, err := t.settingService.GetTgMemory()
+			if err != nil || tgMemory <= 0 || data.Percent <= float64(tgMemory) {
+				return ""
+			}
+			return header + "🔴 " + t.I18nBot("tgbot.messages.memoryThreshold",
+				"Percent=="+strconv.FormatFloat(data.Percent, 'f', 2, 64),
+				"Threshold=="+strconv.Itoa(tgMemory))
+		}
+		return ""
+
 	case eventbus.EventLoginAttempt:
 		if data, ok := e.Data.(*eventbus.LoginEventData); ok {
 			if data.Status == "success" {

+ 4 - 2
internal/web/translation/ar-EG.json

@@ -1309,6 +1309,7 @@
       "smtpErrorRelay": "الخادم يرفض الإرسال من هذا العنوان",
       "smtpErrorEof": "تم إغلاق الاتصال من قبل الخادم",
       "smtpErrorUnknown": "خطأ SMTP: {{ .Error }}",
+      "eventMemoryHigh": "ارتفاع استخدام الذاكرة (%)",
       "remarkTemplate": "قالب الملاحظة",
       "remarkTemplateDesc": "عند تعيينه، يحل هذا محل نموذج الملاحظة لكل رابط اشتراك — اكتب صيغتك الخاصة باستخدام رموز المتغيرات (استخدم الزر لإدراجها). اتركه فارغاً لاستخدام النموذج أعلاه."
     },
@@ -1865,7 +1866,7 @@
       "idDesc": "عرض معرف Telegram الخاص بك"
     },
     "messages": {
-      "cpuThreshold": "🔴 حمل المعالج {{ .Percent }}% عدى الحد المسموح ({{ .Threshold }}%)",
+      "cpuThreshold": "حمل المعالج {{ .Percent }}% عدى الحد المسموح ({{ .Threshold }}%)",
       "selectUserFailed": "❌ حصل خطأ في اختيار المستخدم!",
       "userSaved": "✅ حفظت بيانات مستخدم Telegram.",
       "loginSuccess": "✅ تسجيل الدخول للبانل تم بنجاح.\r\n",
@@ -1940,7 +1941,8 @@
       "eventNodeUp": "العقدة {{ .Name }} متصلة",
       "eventCPUHigh": "ارتفاع استخدام المعالج",
       "eventCPUHighDetail": "المعالج: {{ .Detail }}",
-      "eventLoginFallback": "فشل تسجيل الدخول من {{ .Source }}"
+      "eventLoginFallback": "فشل تسجيل الدخول من {{ .Source }}",
+      "memoryThreshold": "استخدام الذاكرة {{ .Percent }}% يتجاوز الحد {{ .Threshold }}%"
     },
     "buttons": {
       "closeKeyboard": "❌ اقفل الكيبورد",

+ 5 - 3
internal/web/translation/en-US.json

@@ -1418,7 +1418,8 @@
       "smtpErrorTimeout": "Connection timeout — host unreachable",
       "smtpErrorRelay": "Server rejects sending from this address",
       "smtpErrorEof": "Connection closed by server",
-      "smtpErrorUnknown": "SMTP error: {{ .Error }}"
+      "smtpErrorUnknown": "SMTP error: {{ .Error }}",
+      "eventMemoryHigh": "Memory high (%)"
     },
     "xray": {
       "title": "Xray Configs",
@@ -1865,7 +1866,7 @@
       "idDesc": "Show your Telegram ID"
     },
     "messages": {
-      "cpuThreshold": "🔴 CPU Load {{ .Percent }}% exceeds the threshold of {{ .Threshold }}%",
+      "cpuThreshold": "CPU Load {{ .Percent }}% exceeds the threshold of {{ .Threshold }}%",
       "selectUserFailed": "❌ Error in user selection!",
       "userSaved": "✅ Telegram User saved.",
       "loginSuccess": "✅ Logged in to the panel successfully.\r\n",
@@ -1940,7 +1941,8 @@
       "eventNodeUp": "Node {{ .Name }} is UP",
       "eventCPUHigh": "CPU high",
       "eventCPUHighDetail": "CPU: {{ .Detail }}",
-      "eventLoginFallback": "Login failed from {{ .Source }}"
+      "eventLoginFallback": "Login failed from {{ .Source }}",
+      "memoryThreshold": "Memory Load {{ .Percent }}% exceeds the threshold of {{ .Threshold }}%"
     },
     "buttons": {
       "closeKeyboard": "❌ Close Keyboard",

+ 5 - 3
internal/web/translation/es-ES.json

@@ -1309,6 +1309,7 @@
       "smtpErrorRelay": "El servidor rechaza el envío desde esta dirección",
       "smtpErrorEof": "Conexión cerrada por el servidor",
       "smtpErrorUnknown": "Error de SMTP: {{ .Error }}",
+      "eventMemoryHigh": "Uso de memoria alto (%)",
       "remarkTemplate": "Plantilla de notas",
       "remarkTemplateDesc": "Cuando se define, esto reemplaza el modelo de notas para cada enlace de suscripción — escribe tu propio formato con los tokens de variable (usa el botón para insertarlos). Déjalo vacío para usar el modelo anterior."
     },
@@ -1865,7 +1866,7 @@
       "idDesc": "Mostrar tu ID de Telegram"
     },
     "messages": {
-      "cpuThreshold": "🔴 El uso de CPU {{ .Percent }}% es mayor que el umbral {{ .Threshold }}%",
+      "cpuThreshold": "El uso de CPU {{ .Percent }}% es mayor que el umbral {{ .Threshold }}%",
       "selectUserFailed": "❌ ¡Error al seleccionar usuario!",
       "userSaved": "✅ Usuario de Telegram guardado.",
       "loginSuccess": "✅ Has iniciado sesión en el panel con éxito.\r\n",
@@ -1940,7 +1941,8 @@
       "eventNodeUp": "El nodo {{ .Name }} está ACTIVO",
       "eventCPUHigh": "CPU alta",
       "eventCPUHighDetail": "CPU: {{ .Detail }}",
-      "eventLoginFallback": "Inicio de sesión fallido desde {{ .Source }}"
+      "eventLoginFallback": "Inicio de sesión fallido desde {{ .Source }}",
+      "memoryThreshold": "Uso de memoria {{ .Percent }}% supera el umbral de {{ .Threshold }}%"
     },
     "buttons": {
       "closeKeyboard": "❌ Cerrar Teclado",
@@ -2043,4 +2045,4 @@
     "statusDown": "CAÍDO",
     "statusUp": "ACTIVO"
   }
-}
+}

+ 6 - 4
internal/web/translation/fa-IR.json

@@ -1310,7 +1310,8 @@
       "smtpErrorTimeout": "مهلت اتصال به پایان رسید — میزبان در دسترس نیست",
       "smtpErrorRelay": "سرور ارسال از این آدرس را رد می‌کند",
       "smtpErrorEof": "اتصال توسط سرور بسته شد",
-      "smtpErrorUnknown": "خطای SMTP: {{ .Error }}"
+      "smtpErrorUnknown": "خطای SMTP: {{ .Error }}",
+      "eventMemoryHigh": "مصرف حافظه بالا (%)"
     },
     "xray": {
       "title": "پیکربندی ایکس‌ری",
@@ -1865,7 +1866,7 @@
       "idDesc": "نمایش شناسه تلگرام شما"
     },
     "messages": {
-      "cpuThreshold": "🔴 بار ‌پردازنده {{ .Percent }}% بیشتر از آستانه است {{ .Threshold }}%",
+      "cpuThreshold": "بار ‌پردازنده {{ .Percent }}% بیشتر از آستانه است {{ .Threshold }}%",
       "selectUserFailed": "❌ خطا در انتخاب کاربر!",
       "userSaved": "✅ کاربر تلگرام ذخیره شد.",
       "loginSuccess": "✅ با موفقیت به پنل وارد شدید.\r\n",
@@ -1940,7 +1941,8 @@
       "eventNodeUp": "نود {{ .Name }} وصل است",
       "eventCPUHigh": "بالا بودن CPU",
       "eventCPUHighDetail": "CPU: {{ .Detail }}",
-      "eventLoginFallback": "ورود ناموفق از {{ .Source }}"
+      "eventLoginFallback": "ورود ناموفق از {{ .Source }}",
+      "memoryThreshold": "مصرف حافظه {{ .Percent }}% از حد آستانه {{ .Threshold }}% فراتر رفته است"
     },
     "buttons": {
       "closeKeyboard": "❌ بستن کیبورد",
@@ -2043,4 +2045,4 @@
     "statusDown": "قطع",
     "statusUp": "وصل"
   }
-}
+}

+ 4 - 2
internal/web/translation/id-ID.json

@@ -1309,6 +1309,7 @@
       "smtpErrorRelay": "Server menolak pengiriman dari alamat ini",
       "smtpErrorEof": "Koneksi ditutup oleh server",
       "smtpErrorUnknown": "Kesalahan SMTP: {{ .Error }}",
+      "eventMemoryHigh": "Penggunaan memori tinggi (%)",
       "remarkTemplate": "Templat Catatan",
       "remarkTemplateDesc": "Jika diatur, ini menggantikan model catatan untuk setiap tautan langganan — tulis format Anda sendiri dengan token variabel (gunakan tombol untuk menyisipkannya). Biarkan kosong untuk memakai model di atas."
     },
@@ -1865,7 +1866,7 @@
       "idDesc": "Tampilkan ID Telegram Anda"
     },
     "messages": {
-      "cpuThreshold": "🔴 Beban CPU {{ .Percent }}% melebihi batas {{ .Threshold }}%",
+      "cpuThreshold": "Beban CPU {{ .Percent }}% melebihi batas {{ .Threshold }}%",
       "selectUserFailed": "❌ Kesalahan dalam pemilihan pengguna!",
       "userSaved": "✅ Pengguna Telegram tersimpan.",
       "loginSuccess": "✅ Berhasil masuk ke panel.\r\n",
@@ -1940,7 +1941,8 @@
       "eventNodeUp": "Node {{ .Name }} AKTIF",
       "eventCPUHigh": "CPU tinggi",
       "eventCPUHighDetail": "CPU: {{ .Detail }}",
-      "eventLoginFallback": "Gagal masuk dari {{ .Source }}"
+      "eventLoginFallback": "Gagal masuk dari {{ .Source }}",
+      "memoryThreshold": "Penggunaan memori {{ .Percent }}% melebihi ambang batas {{ .Threshold }}%"
     },
     "buttons": {
       "closeKeyboard": "❌ Tutup Papan Ketik",

+ 5 - 3
internal/web/translation/ja-JP.json

@@ -1309,6 +1309,7 @@
       "smtpErrorRelay": "サーバーはこのアドレスからの送信を拒否しています",
       "smtpErrorEof": "サーバーによって接続が閉じられました",
       "smtpErrorUnknown": "SMTPエラー: {{ .Error }}",
+      "eventMemoryHigh": "メモリ使用率が高い (%)",
       "remarkTemplate": "備考テンプレート",
       "remarkTemplateDesc": "設定すると、すべてのサブスクリプションリンクの備考モデルを置き換えます — 変数トークンを使って独自の形式を記述してください(ボタンで挿入できます)。空欄にすると上記のモデルが使用されます。"
     },
@@ -1865,7 +1866,7 @@
       "idDesc": "Telegram IDを表示"
     },
     "messages": {
-      "cpuThreshold": "🔴 CPU使用率は{{ .Percent }}%、しきい値{{ .Threshold }}%を超えました",
+      "cpuThreshold": "CPU使用率は{{ .Percent }}%、しきい値{{ .Threshold }}%を超えました",
       "selectUserFailed": "❌ ユーザーの選択に失敗しました!",
       "userSaved": "✅ Telegramユーザーが保存されました。",
       "loginSuccess": "✅ パネルに正常にログインしました。\r\n",
@@ -1940,7 +1941,8 @@
       "eventNodeUp": "ノード {{ .Name }} が復旧しました",
       "eventCPUHigh": "CPU高負荷",
       "eventCPUHighDetail": "CPU: {{ .Detail }}",
-      "eventLoginFallback": "{{ .Source }} からのログインに失敗しました"
+      "eventLoginFallback": "{{ .Source }} からのログインに失敗しました",
+      "memoryThreshold": "メモリ使用率 {{ .Percent }}% がしきい値 {{ .Threshold }}% を超えました"
     },
     "buttons": {
       "closeKeyboard": "❌ キーボードを閉じる",
@@ -2043,4 +2045,4 @@
     "statusDown": "ダウン",
     "statusUp": "アップ"
   }
-}
+}

+ 5 - 3
internal/web/translation/pt-BR.json

@@ -1309,6 +1309,7 @@
       "smtpErrorRelay": "O servidor rejeita o envio a partir deste endereço",
       "smtpErrorEof": "Conexão encerrada pelo servidor",
       "smtpErrorUnknown": "Erro de SMTP: {{ .Error }}",
+      "eventMemoryHigh": "Uso de memória alto (%)",
       "remarkTemplate": "Modelo de Observação",
       "remarkTemplateDesc": "Quando definido, isto substitui o modelo de observação de cada link de assinatura — escreva seu próprio formato com os tokens de variáveis (use o botão para inseri-los). Deixe vazio para usar o modelo acima."
     },
@@ -1865,7 +1866,7 @@
       "idDesc": "Mostrar seu ID do Telegram"
     },
     "messages": {
-      "cpuThreshold": "🔴 A carga da CPU {{ .Percent }}% excede o limite de {{ .Threshold }}%",
+      "cpuThreshold": "A carga da CPU {{ .Percent }}% excede o limite de {{ .Threshold }}%",
       "selectUserFailed": "❌ Erro na seleção do usuário!",
       "userSaved": "✅ Usuário do Telegram salvo.",
       "loginSuccess": "✅ Conectado ao painel com sucesso.\r\n",
@@ -1940,7 +1941,8 @@
       "eventNodeUp": "O nó {{ .Name }} está ATIVO",
       "eventCPUHigh": "CPU alta",
       "eventCPUHighDetail": "CPU: {{ .Detail }}",
-      "eventLoginFallback": "Falha de login a partir de {{ .Source }}"
+      "eventLoginFallback": "Falha de login a partir de {{ .Source }}",
+      "memoryThreshold": "Uso de memória {{ .Percent }}% excede o limite de {{ .Threshold }}%"
     },
     "buttons": {
       "closeKeyboard": "❌ Fechar teclado",
@@ -2043,4 +2045,4 @@
     "statusDown": "INATIVO",
     "statusUp": "ATIVO"
   }
-}
+}

+ 5 - 3
internal/web/translation/ru-RU.json

@@ -1309,6 +1309,7 @@
       "smtpErrorRelay": "Сервер отклоняет отправку с этого адреса",
       "smtpErrorEof": "Соединение закрыто сервером",
       "smtpErrorUnknown": "Ошибка SMTP: {{ .Error }}",
+      "eventMemoryHigh": "Превышение порога памяти (%)",
       "remarkTemplate": "Шаблон примечания",
       "remarkTemplateDesc": "Если задан, заменяет модель примечания для каждой ссылки подписки — задайте собственный формат с помощью токенов переменных (используйте кнопку для их вставки). Оставьте пустым, чтобы использовать модель выше."
     },
@@ -1865,7 +1866,7 @@
       "idDesc": "Показать ваш Telegram ID"
     },
     "messages": {
-      "cpuThreshold": "🔴 Загрузка процессора составляет {{ .Percent }}%, что превышает пороговое значение {{ .Threshold }}%",
+      "cpuThreshold": "Загрузка процессора составляет {{ .Percent }}%, что превышает пороговое значение {{ .Threshold }}%",
       "selectUserFailed": "❌ Ошибка при выборе пользователя.",
       "userSaved": "✅ Пользователь Telegram сохранен.",
       "loginSuccess": "✅ Успешный вход в панель.\r\n",
@@ -1940,7 +1941,8 @@
       "eventNodeUp": "Узел {{ .Name }} В СЕТИ",
       "eventCPUHigh": "Высокая загрузка CPU",
       "eventCPUHighDetail": "CPU: {{ .Detail }}",
-      "eventLoginFallback": "Неудачный вход с {{ .Source }}"
+      "eventLoginFallback": "Неудачный вход с {{ .Source }}",
+      "memoryThreshold": "🔴 Использование памяти {{ .Percent }}% превышает пороговое значение {{ .Threshold }}%"
     },
     "buttons": {
       "closeKeyboard": "❌ Закрыть клавиатуру",
@@ -2043,4 +2045,4 @@
     "statusDown": "НЕДОСТУПЕН",
     "statusUp": "РАБОТАЕТ"
   }
-}
+}

+ 4 - 2
internal/web/translation/tr-TR.json

@@ -1309,6 +1309,7 @@
       "smtpErrorRelay": "Sunucu bu adresten gönderimi reddediyor",
       "smtpErrorEof": "Bağlantı sunucu tarafından kapatıldı",
       "smtpErrorUnknown": "SMTP hatası: {{ .Error }}",
+      "eventMemoryHigh": "Bellek kullanımı yüksek (%)",
       "remarkTemplate": "Açıklama Şablonu",
       "remarkTemplateDesc": "Ayarlandığında, her abonelik bağlantısının açıklama modelinin yerini alır — değişken belirteçleriyle kendi formatınızı yazın (eklemek için düğmeyi kullanın). Yukarıdaki modeli kullanmak için boş bırakın."
     },
@@ -1865,7 +1866,7 @@
       "idDesc": "Telegram Kimliğinizi gösterir"
     },
     "messages": {
-      "cpuThreshold": "🔴 CPU Yükü ({{ .Percent }}%), {{ .Threshold }}% eşiğini aşıyor",
+      "cpuThreshold": "CPU Yükü ({{ .Percent }}%), {{ .Threshold }}% eşiğini aşıyor",
       "selectUserFailed": "❌ Kullanıcı seçiminde hata!",
       "userSaved": "✅ Telegram Kullanıcısı kaydedildi.",
       "loginSuccess": "✅ Panele başarıyla giriş yapıldı.\r\n",
@@ -1940,7 +1941,8 @@
       "eventNodeUp": "{{ .Name }} düğümü ÇEVRİMİÇİ",
       "eventCPUHigh": "Yüksek CPU",
       "eventCPUHighDetail": "CPU: {{ .Detail }}",
-      "eventLoginFallback": "{{ .Source }} adresinden oturum açma başarısız"
+      "eventLoginFallback": "{{ .Source }} adresinden oturum açma başarısız",
+      "memoryThreshold": "Bellek kullanımı {{ .Percent }}% eşiği {{ .Threshold }}% aşıyor"
     },
     "buttons": {
       "closeKeyboard": "❌ Klavyeyi Kapat",

+ 5 - 3
internal/web/translation/uk-UA.json

@@ -1309,6 +1309,7 @@
       "smtpErrorRelay": "Сервер відхиляє надсилання з цієї адреси",
       "smtpErrorEof": "З'єднання закрито сервером",
       "smtpErrorUnknown": "Помилка SMTP: {{ .Error }}",
+      "eventMemoryHigh": "Високе використання пам'яті (%)",
       "remarkTemplate": "Шаблон примітки",
       "remarkTemplateDesc": "Якщо задано, це замінює модель примітки для кожного посилання підписки — напишіть власний формат із токенами змінних (використовуйте кнопку для їх вставлення). Залиште порожнім, щоб використовувати модель вище."
     },
@@ -1865,7 +1866,7 @@
       "idDesc": "Показати ваш Telegram ID"
     },
     "messages": {
-      "cpuThreshold": "🔴 Навантаження ЦП  {{ .Percent }}% перевищує порогове значення {{ .Threshold }}%",
+      "cpuThreshold": "Навантаження ЦП  {{ .Percent }}% перевищує порогове значення {{ .Threshold }}%",
       "selectUserFailed": "❌ Помилка під час вибору користувача!",
       "userSaved": "✅ Користувача Telegram збережено.",
       "loginSuccess": "✅ Успішно ввійшли в панель\r\n",
@@ -1940,7 +1941,8 @@
       "eventNodeUp": "Вузол {{ .Name }} ДОСТУПНИЙ",
       "eventCPUHigh": "Високе навантаження на CPU",
       "eventCPUHighDetail": "CPU: {{ .Detail }}",
-      "eventLoginFallback": "Невдала спроба входу з {{ .Source }}"
+      "eventLoginFallback": "Невдала спроба входу з {{ .Source }}",
+      "memoryThreshold": "Використання пам'яті {{ .Percent }}% перевищує порогове значення {{ .Threshold }}%"
     },
     "buttons": {
       "closeKeyboard": "❌ Закрити клавіатуру",
@@ -2043,4 +2045,4 @@
     "statusDown": "НЕДОСТУПНО",
     "statusUp": "ДОСТУПНО"
   }
-}
+}

+ 5 - 3
internal/web/translation/vi-VN.json

@@ -1309,6 +1309,7 @@
       "smtpErrorRelay": "Máy chủ từ chối gửi từ địa chỉ này",
       "smtpErrorEof": "Kết nối đã bị máy chủ đóng",
       "smtpErrorUnknown": "Lỗi SMTP: {{ .Error }}",
+      "eventMemoryHigh": "Sử dụng bộ nhớ cao (%)",
       "remarkTemplate": "Mẫu ghi chú",
       "remarkTemplateDesc": "Khi được đặt, mục này thay thế mô hình ghi chú cho mọi liên kết đăng ký — hãy viết định dạng riêng của bạn bằng các token biến (dùng nút để chèn chúng). Để trống để dùng mô hình ở trên."
     },
@@ -1865,7 +1866,7 @@
       "idDesc": "Hiển thị ID Telegram của bạn"
     },
     "messages": {
-      "cpuThreshold": "🔴 Sử dụng CPU {{ .Percent }}% vượt quá ngưỡng {{ .Threshold }}%",
+      "cpuThreshold": "Sử dụng CPU {{ .Percent }}% vượt quá ngưỡng {{ .Threshold }}%",
       "selectUserFailed": "❌ Lỗi khi chọn người dùng!",
       "userSaved": "✅ Người dùng Telegram đã được lưu.",
       "loginSuccess": "✅ Đăng nhập thành công vào bảng điều khiển.\r\n",
@@ -1940,7 +1941,8 @@
       "eventNodeUp": "Node {{ .Name }} đã HOẠT ĐỘNG",
       "eventCPUHigh": "CPU cao",
       "eventCPUHighDetail": "CPU: {{ .Detail }}",
-      "eventLoginFallback": "Đăng nhập thất bại từ {{ .Source }}"
+      "eventLoginFallback": "Đăng nhập thất bại từ {{ .Source }}",
+      "memoryThreshold": "Sử dụng bộ nhớ {{ .Percent }}% vượt quá ngưỡng {{ .Threshold }}%"
     },
     "buttons": {
       "closeKeyboard": "❌ Đóng Bàn Phím",
@@ -2043,4 +2045,4 @@
     "statusDown": "NGỪNG HOẠT ĐỘNG",
     "statusUp": "HOẠT ĐỘNG"
   }
-}
+}

+ 4 - 2
internal/web/translation/zh-CN.json

@@ -1309,6 +1309,7 @@
       "smtpErrorRelay": "服务器拒绝从此地址发送",
       "smtpErrorEof": "连接被服务器关闭",
       "smtpErrorUnknown": "SMTP 错误:{{ .Error }}",
+      "eventMemoryHigh": "内存使用率高 (%)",
       "remarkTemplate": "备注模板",
       "remarkTemplateDesc": "设置后,将替换每个订阅链接的备注模型 — 使用变量标记编写您自己的格式(用按钮插入它们)。留空则使用上方的模型。"
     },
@@ -1865,7 +1866,7 @@
       "idDesc": "显示您的 Telegram ID"
     },
     "messages": {
-      "cpuThreshold": "🔴 CPU 使用率为 {{ .Percent }}%,超过阈值 {{ .Threshold }}%",
+      "cpuThreshold": "CPU 使用率为 {{ .Percent }}%,超过阈值 {{ .Threshold }}%",
       "selectUserFailed": "❌ 用户选择错误!",
       "userSaved": "✅ 电报用户已保存。",
       "loginSuccess": "✅ 成功登录到面板。\r\n",
@@ -1940,7 +1941,8 @@
       "eventNodeUp": "节点 {{ .Name }} 已上线",
       "eventCPUHigh": "CPU 占用过高",
       "eventCPUHighDetail": "CPU:{{ .Detail }}",
-      "eventLoginFallback": "来自 {{ .Source }} 的登录失败"
+      "eventLoginFallback": "来自 {{ .Source }} 的登录失败",
+      "memoryThreshold": "内存使用率 {{ .Percent }}% 超过阈值 {{ .Threshold }}%"
     },
     "buttons": {
       "closeKeyboard": "❌ 关闭键盘",

+ 5 - 3
internal/web/translation/zh-TW.json

@@ -1309,6 +1309,7 @@
       "smtpErrorRelay": "伺服器拒絕從此地址傳送",
       "smtpErrorEof": "連線已被伺服器關閉",
       "smtpErrorUnknown": "SMTP 錯誤:{{ .Error }}",
+      "eventMemoryHigh": "記憶體使用率高 (%)",
       "remarkTemplate": "備註範本",
       "remarkTemplateDesc": "設定後,這將取代每個訂閱連結的備註模型——使用變數標記撰寫您自己的格式(使用按鈕來插入)。留空則使用上方的模型。"
     },
@@ -1865,7 +1866,7 @@
       "idDesc": "顯示您的 Telegram ID"
     },
     "messages": {
-      "cpuThreshold": "🔴 CPU 使用率為 {{ .Percent }}%,超過閾值 {{ .Threshold }}%",
+      "cpuThreshold": "CPU 使用率為 {{ .Percent }}%,超過閾值 {{ .Threshold }}%",
       "selectUserFailed": "❌ 使用者選擇錯誤!",
       "userSaved": "✅ 電報使用者已儲存。",
       "loginSuccess": "✅ 成功登入到面板。\r\n",
@@ -1940,7 +1941,8 @@
       "eventNodeUp": "節點 {{ .Name }} 已上線",
       "eventCPUHigh": "CPU 偏高",
       "eventCPUHighDetail": "CPU:{{ .Detail }}",
-      "eventLoginFallback": "來自 {{ .Source }} 的登入失敗"
+      "eventLoginFallback": "來自 {{ .Source }} 的登入失敗",
+      "memoryThreshold": "記憶體使用率 {{ .Percent }}% 超過閾值 {{ .Threshold }}%"
     },
     "buttons": {
       "closeKeyboard": "❌ 關閉鍵盤",
@@ -2043,4 +2045,4 @@
     "statusDown": "中斷",
     "statusUp": "恢復"
   }
-}
+}

+ 36 - 1
internal/web/web.go

@@ -290,7 +290,8 @@ const (
 	cadenceCheckHash     = "@every 2m"
 	// cpu.Percent samples over a full minute (blocking), so a finer cadence just
 	// stacks overlapping samplers; subscribers rate-limit alerts to 1/min anyway.
-	cadenceCPUAlarm = "@every 1m"
+	cadenceCPUAlarm    = "@every 1m"
+	cadenceMemoryAlarm = "@every 1m"
 )
 
 // startTask schedules background jobs (Xray checks, traffic jobs, cron
@@ -385,6 +386,10 @@ func (s *Server) startTask(restartXray bool) {
 	if s.cpuAlarmWanted() {
 		s.cron.AddJob(cadenceCPUAlarm, job.NewCheckCpuJob())
 	}
+	// Memory monitor publishes memory.high events; register it whenever any notifier wants them.
+	if s.memoryAlarmWanted() {
+		s.cron.AddJob(cadenceMemoryAlarm, job.NewCheckMemJob())
+	}
 }
 
 // cpuAlarmWanted reports whether any notifier is configured to receive cpu.high
@@ -418,6 +423,36 @@ func (s *Server) cpuAlarmWanted() bool {
 	return false
 }
 
+// memoryAlarmWanted reports whether any notifier is configured to receive memory.high alerts.
+func (s *Server) memoryAlarmWanted() bool {
+	wants := func(events string, threshold int) bool {
+		if threshold <= 0 {
+			return false
+		}
+		for _, e := range strings.Split(events, ",") {
+			if strings.TrimSpace(e) == string(eventbus.EventMemoryHigh) {
+				return true
+			}
+		}
+		return false
+	}
+	if on, _ := s.settingService.GetTgbotEnabled(); on {
+		events, _ := s.settingService.GetTgEnabledEvents()
+		mem, _ := s.settingService.GetTgMemory()
+		if wants(events, mem) {
+			return true
+		}
+	}
+	if on, _ := s.settingService.GetSmtpEnable(); on {
+		events, _ := s.settingService.GetSmtpEnabledEvents()
+		mem, _ := s.settingService.GetSmtpMemory()
+		if wants(events, mem) {
+			return true
+		}
+	}
+	return false
+}
+
 // Start initializes and starts the web server with configured settings, routes, and background jobs.
 func (s *Server) Start() (err error) {
 	return s.start(true, true)