feat(notifications): event bus architecture with Telegram and SMTP subscribers (#5326)

* feat(notifications): event bus architecture with Telegram and SMTP subscribers

- Event bus core with buffered channel, fan-out, panic recovery
- Telegram subscriber with HTML formatting and rate limiting
- Email subscriber with SMTP/TLS/STARTTLS support and stage diagnostics
- 5 event types: outbound.down/up, xray.crash, cpu.high, login.attempt
- CPU threshold checks per subscriber (tgCpu for TG, smtpCpu for Email)
- SystemMetricData struct for raw metric values in events
- i18n keys for en-US, ru-RU, and English defaults for other locales

* fix

* fix(notifications): repair crash/CPU alerts, harden secrets, add node alerts

Bug fixes:
- Xray crash notifications were permanently suppressed after the first crash:
  XrayStateTracker latched state="down" with no reset and no recovery event,
  so only the first crash per process lifetime ever notified. Removed the
  tracker; the existing 1/min rate limiter already dedupes crash-loop spam.
- Email CPU alerts could never fire unless Telegram was also enabled, because
  the CPU job was registered only inside the tgbot block. Register it whenever
  either Telegram or SMTP wants cpu.high (new cpuAlarmWanted gate) and relax
  the cadence to @every 1m (cpu.Percent already samples over a full minute).
- SMTP password (and, pre-existing, all other secrets) were shipped to the
  browser in plaintext: GetAllSettingView was dead code and /setting/all
  returned the raw model. Wire getAllSetting -> GetAllSettingView, redact
  smtpPassword with a hasSmtpPassword presence flag, and preserve it on blank
  save. Closes the leak for tgBotToken/ldapPassword/2FA token too.

Polish:
- email Send: use nil SMTP auth when no credentials (Go refuses PlainAuth over
  the unencrypted "none" transport).
- Remove unused EventClientDepleted; fix inaccurate bus.go doc comments; drop
  stale tgBotLoginNotify from the frontend schema; gofmt alignment.

Feature - node online/offline alerts:
- Emit node.down/node.up from the heartbeat job on a real status transition
  (with a startup-spam guard), reusing NodeHealthData. Formatted by both the
  Telegram and email subscribers and selectable in the settings UI.

Regenerated frontend types (hasSmtpPassword). New i18n keys added to en-US;
other locales fall back to English (bundle default) until translated.

* fix(settings): use antd Space orientation instead of deprecated direction

Ant Design 6 deprecated Space's `direction` prop in favor of `orientation`,
which logged a console warning from the Telegram/Email notification tabs. Brings
these two tabs in line with the rest of the codebase, which already uses
`orientation`.

* i18n(notifications): translate the notification feature into all locales

The notifications PR shipped ~99 new strings (SMTP settings, event labels,
Telegram/email message templates) as English placeholders in every non-English
locale. Translate them — plus the node-alert keys added during this review —
into all 12 locales: Arabic, Spanish, Persian, Indonesian, Japanese,
Portuguese-BR, Russian, Turkish, Ukrainian, Vietnamese, and Simplified/
Traditional Chinese.

Go-template placeholders ({{ .Tag }}, {{ .Name }}, etc.) are preserved exactly;
tgbot message values carry no leading status emoji (the bot/email code adds
those, so an emoji in the value would duplicate it); product/protocol names
(SMTP, STARTTLS, TLS, CPU, Xray, Telegram) are kept as-is.

---------

Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
This commit is contained in:
Sentiago
2026-06-15 22:03:41 +03:00
committed by GitHub
parent 7fe082a7f1
commit eec030f86f
48 changed files with 3854 additions and 141 deletions
+127 -2
View File
@@ -1200,7 +1200,69 @@
"userPassMustBeNotEmpty": "新使用者名稱和新密碼不能為空",
"getOutboundTrafficError": "取得出站流量錯誤",
"resetOutboundTrafficError": "重設出站流量錯誤"
}
},
"emailNotifications": "通知",
"emailSettings": "電子郵件",
"eventCPUHigh": "CPU 偏高(%",
"eventGroupOutbound": "出站",
"eventGroupSecurity": "安全性",
"eventGroupSystem": "系統",
"eventGroupXray": "Xray 核心",
"eventLoginAttempt": "登入嘗試",
"eventOutboundDown": "中斷",
"eventOutboundUp": "恢復",
"eventXrayCrash": "當機",
"requestFailed": "請求失敗",
"smtpEnable": "啟用電子郵件通知",
"smtpEnableDesc": "透過 SMTP 啟用電子郵件通知",
"smtpEncryption": "加密",
"smtpEncryptionDesc": "SMTP 連線加密方式",
"smtpEncryptionNone": "無(純文字)",
"smtpEncryptionStartTLS": "STARTTLS",
"smtpEncryptionTLS": "TLS(隱含)",
"smtpEventBusNotify": "電子郵件事件通知",
"smtpEventBusNotifyDesc": "選擇觸發電子郵件通知的事件",
"smtpHost": "SMTP 主機",
"smtpHostDesc": "SMTP 伺服器主機名稱(例如 smtp.gmail.com",
"smtpHostNotConfigured": "尚未設定 SMTP 主機",
"smtpNoRecipients": "尚未設定收件人",
"smtpNotInitialized": "SMTP 尚未初始化",
"smtpPassword": "SMTP 密碼",
"smtpPasswordDesc": "SMTP 驗證密碼",
"smtpPort": "SMTP 連接埠",
"smtpPortDesc": "SMTP 伺服器連接埠(預設:587",
"smtpSettings": "SMTP 設定",
"smtpStageAuth": "驗證",
"smtpStageConnect": "連線",
"smtpStageSend": "傳送",
"smtpTestSuccess": "測試郵件已成功傳送",
"smtpTo": "收件人",
"smtpToDesc": "以逗號分隔的收件人電子郵件地址",
"smtpUsername": "SMTP 使用者名稱",
"smtpUsernameDesc": "SMTP 驗證使用者名稱",
"telegramTokenConfigured": "已設定;留空以保留目前的權杖。",
"telegramTokenPlaceholder": "已設定 - 輸入新權杖以取代",
"testSmtp": "傳送測試郵件",
"testTgBot": "傳送測試訊息",
"tgBotNotEnabled": "Telegram 機器人未啟用",
"tgBotNotRunning": "Telegram 機器人未執行",
"tgEventBusNotify": "Telegram 事件通知",
"tgEventBusNotifyDesc": "選擇觸發 Telegram 通知的事件",
"tgTestFailed": "Telegram 測試失敗",
"tgTestSuccess": "測試訊息已傳送至 Telegram",
"smtpErrorAuth": "驗證失敗 — 請檢查使用者名稱和密碼",
"smtpErrorStarttls": "伺服器需要 STARTTLS — 請變更加密類型",
"smtpErrorTls": "伺服器需要 TLS — 請變更加密類型",
"smtpErrorRefused": "連線遭拒 — 請檢查主機和連接埠",
"smtpErrorTimeout": "連線逾時 — 無法連線至主機",
"smtpErrorRelay": "伺服器拒絕從此地址傳送",
"smtpErrorEof": "連線已被伺服器關閉",
"smtpErrorUnknown": "SMTP 錯誤:{{ .Error }}",
"eventGroupNode": "節點",
"eventNodeDown": "離線",
"eventNodeUp": "上線",
"smtpPasswordConfigured": "已設定;留空以保留目前的密碼。",
"smtpPasswordPlaceholder": "已設定 - 輸入新密碼以取代"
},
"xray": {
"title": "Xray 配置",
@@ -1703,7 +1765,18 @@
"AreYouSure": "你確定嗎?🤔",
"SuccessResetTraffic": "📧 電子郵件: {{ .ClientEmail }}\n🏁 結果: ✅ 成功",
"FailedResetTraffic": "📧 電子郵件: {{ .ClientEmail }}\n🏁 結果: ❌ 失敗 \n\n🛠️ 錯誤: [ {{ .ErrorMessage }} ]",
"FinishProcess": "🔚 所有客戶的流量重置已完成。"
"FinishProcess": "🔚 所有客戶的流量重置已完成。",
"eventCPUHigh": "CPU 偏高",
"eventCPUHighDetail": "CPU{{ .Detail }}",
"eventDelayDetail": "延遲:{{ .Delay }} 毫秒",
"eventErrorDetail": "錯誤:{{ .Error }}",
"eventLoginFallback": "來自 {{ .Source }} 的登入失敗",
"eventOutboundDown": "出站 {{ .Tag }} 已中斷",
"eventOutboundUp": "出站 {{ .Tag }} 已恢復",
"eventXrayCrash": "Xray 已當機",
"eventXrayCrashError": "錯誤:{{ .Error }}",
"eventNodeDown": "節點 {{ .Name }} 已離線",
"eventNodeUp": "節點 {{ .Name }} 已上線"
},
"buttons": {
"closeKeyboard": "❌ 關閉鍵盤",
@@ -1773,5 +1846,57 @@
"chooseClient": "為入站 {{ .Inbound }} 選擇一個客戶",
"chooseInbound": "選擇一個入站"
}
},
"email": {
"labelDelay": "延遲",
"labelDetail": "詳細資訊",
"labelError": "錯誤",
"labelIP": "IP",
"labelOutbound": "出站",
"labelReason": "原因",
"labelSource": "來源",
"labelStatus": "狀態",
"labelTime": "時間",
"labelUsername": "使用者名稱",
"statusBanned": "BANNED",
"statusCrashed": "已當機",
"statusDown": "中斷",
"statusFailed": "失敗",
"statusFull": "FULL",
"statusHigh": "偏高",
"statusOffline": "OFFLINE",
"statusOnline": "ONLINE",
"statusRunning": "執行中",
"statusSuccess": "成功",
"statusUp": "恢復",
"statusXrayDown": "Xray DOWN",
"statusXrayUp": "Xray UP",
"subjectCPUHigh": "CPU 偏高",
"subjectDiskFull": "Disk full",
"subjectIPBanned": "IP banned: {{ .IP }}",
"subjectLoginFailed": "登入失敗",
"subjectLoginSuccess": "登入成功",
"subjectNodeOffline": "Node {{ .Node }} is OFFLINE",
"subjectNodeOnline": "Node {{ .Node }} is ONLINE",
"subjectNodeXrayDown": "Node {{ .Node }} Xray is DOWN",
"subjectNodeXrayUp": "Node {{ .Node }} Xray is UP",
"subjectOutboundDown": "出站 {{ .Tag }} 已中斷",
"subjectOutboundUp": "出站 {{ .Tag }} 已恢復",
"subjectXrayCrash": "Xray 已當機",
"subjectXrayUp": "Xray is UP",
"titleCPUHigh": "CPU 偏高",
"titleDiskFull": "Disk full",
"titleIPBanned": "IP banned",
"titleLoginFailed": "登入失敗",
"titleLoginSuccess": "登入成功",
"titleNodeOffline": "Node OFFLINE",
"titleNodeOnline": "Node ONLINE",
"titleNodeXrayDown": "Node Xray DOWN",
"titleNodeXrayUp": "Node Xray UP",
"titleOutboundDown": "出站中斷",
"titleOutboundUp": "出站恢復",
"titleXrayCrash": "Xray 已當機",
"titleXrayUp": "Xray UP",
"labelNode": "節點"
}
}