mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-24 20:07:13 +00:00
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:
@@ -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 }}ms",
|
||||
"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": "ノード"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user