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
+128 -3
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 (پیش‌فرض: ۵۸۷)",
"smtpSettings": "تنظیمات SMTP",
"smtpStageAuth": "احراز هویت",
"smtpStageConnect": "اتصال",
"smtpStageSend": "ارسال",
"smtpTestSuccess": "ایمیل آزمایشی با موفقیت ارسال شد",
"smtpTo": "گیرندگان",
"smtpToDesc": "آدرس‌های ایمیل گیرندگان، جداشده با کاما",
"smtpUsername": "نام‌کاربری SMTP",
"smtpUsernameDesc": "نام‌کاربری احراز هویت SMTP",
"telegramTokenConfigured": "پیکربندی شده؛ برای حفظ توکن فعلی خالی بگذارید.",
"telegramTokenPlaceholder": "پیکربندی شده - برای جایگزینی، توکن جدید وارد کنید",
"testSmtp": "ارسال ایمیل آزمایشی",
"testTgBot": "ارسال پیام آزمایشی",
"tgBotNotEnabled": "ربات تلگرام فعال نیست",
"tgBotNotRunning": "ربات تلگرام در حال اجرا نیست",
"tgEventBusNotify": "اعلان‌های رویداد تلگرام",
"tgEventBusNotifyDesc": "انتخاب کنید کدام رویدادها اعلان تلگرام را فعال می‌کنند",
"tgTestFailed": "آزمایش تلگرام ناموفق بود",
"tgTestSuccess": "پیام آزمایشی به تلگرام ارسال شد",
"smtpErrorAuth": "احراز هویت ناموفق بود — نام‌کاربری و رمز عبور را بررسی کنید",
"smtpErrorStarttls": "سرور به STARTTLS نیاز دارد — نوع رمزنگاری را تغییر دهید",
"smtpErrorTls": "سرور به TLS نیاز دارد — نوع رمزنگاری را تغییر دهید",
"smtpErrorRefused": "اتصال رد شد — میزبان و پورت را بررسی کنید",
"smtpErrorTimeout": "مهلت اتصال به پایان رسید — میزبان در دسترس نیست",
"smtpErrorRelay": "سرور ارسال از این آدرس را رد می‌کند",
"smtpErrorEof": "اتصال توسط سرور بسته شد",
"smtpErrorUnknown": "خطای SMTP: {{ .Error }}",
"eventGroupNode": "نودها",
"eventNodeDown": "قطع",
"eventNodeUp": "وصل",
"smtpPasswordConfigured": "پیکربندی شده؛ برای حفظ رمز عبور فعلی خالی بگذارید.",
"smtpPasswordPlaceholder": "پیکربندی شده - برای جایگزینی، رمز عبور جدید وارد کنید"
},
"xray": {
"title": "پیکربندی ایکس‌ری",
@@ -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": "نود"
}
}
}