mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-05 05:32:07 +03:00
2b1308ca29
Problem: a flaky outbound produces hundreds of false-positive "outbound down" notifications overnight — each fires the moment xray's observatory reports a single failed probe, and the next successful probe fires an "up". applyObservatory forwarded every raw alive:true->false transition straight to EventOutboundDown; xray's observatory has effectively no hysteresis, and nothing on the panel side debounced it (the email/Telegram subscribers are pure formatters). Fix: debounce per outbound. outbound.down now fires only after outboundDownThreshold consecutive FAILED probes (new setting, default 3); outbound.up fires immediately on the first successful probe and only when a down was actually notified. The threshold gates the event itself, so email and Telegram share one knob (exposed next to the outbound.down toggle). The streak counts genuinely new probes (last_try_time advancing), not sampler polls — the sampler runs every 2s but the observatory re-probes per its probeInterval, so counting samples would trip the threshold instantly. outboundDownThreshold=1 reproduces the legacy notify-on-first-failure behaviour. Tuning the observatory's probe interval/timeout is not a workaround: those probes also drive the load balancer's outbound selection, so loosening them to quiet notifications would slow real failover away from a genuinely dead outbound. Notifications don't need observatory-grade latency, so the tolerance belongs at the notification layer, leaving the observatory (and balancer) untouched. Adds TestApplyObservatoryDebounce covering the threshold, probe-vs-sample counting, single-blip suppression and the legacy path. Co-authored-by: Yuriy Khachaturian <y.khachaturian@souzmult.ru> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
110 lines
3.3 KiB
TypeScript
110 lines
3.3 KiB
TypeScript
import { InputNumber } from 'antd';
|
|
import { CloudServerOutlined, ThunderboltOutlined, DesktopOutlined, DashboardOutlined, SafetyOutlined } from '@ant-design/icons';
|
|
import type { AllSetting } from '@/models/setting';
|
|
import { NotificationLayout } from './NotificationLayout';
|
|
import { NotificationGroup } from './NotificationGroup';
|
|
import type { NotificationGroupConfig } from './types';
|
|
|
|
const GROUPS: NotificationGroupConfig[] = [
|
|
{
|
|
icon: <CloudServerOutlined />,
|
|
title: 'eventGroupOutbound',
|
|
events: [
|
|
{
|
|
key: 'outbound.down',
|
|
label: 'eventOutboundDown',
|
|
settingKey: 'outboundDownThreshold',
|
|
extra: ({ value, onChange, ariaLabel }) => (
|
|
<InputNumber size="small" min={1} max={100} value={value} onChange={onChange} aria-label={ariaLabel} style={{ width: 80 }} />
|
|
),
|
|
},
|
|
{ key: 'outbound.up', label: 'eventOutboundUp', settingKey: '' },
|
|
],
|
|
},
|
|
{
|
|
icon: <ThunderboltOutlined />,
|
|
title: 'eventGroupXray',
|
|
events: [
|
|
{ key: 'xray.crash', label: 'eventXrayCrash', settingKey: '' },
|
|
],
|
|
},
|
|
{
|
|
icon: <DesktopOutlined />,
|
|
title: 'eventGroupNode',
|
|
events: [
|
|
{ key: 'node.down', label: 'eventNodeDown', settingKey: '' },
|
|
{ key: 'node.up', label: 'eventNodeUp', settingKey: '' },
|
|
],
|
|
},
|
|
{
|
|
icon: <DashboardOutlined />,
|
|
title: 'eventGroupSystem',
|
|
events: [
|
|
{
|
|
key: 'cpu.high',
|
|
label: 'eventCPUHigh',
|
|
settingKey: 'tgCpu',
|
|
extra: ({ value, onChange, ariaLabel }) => (
|
|
<InputNumber size="small" min={0} max={100} value={value} onChange={onChange} aria-label={ariaLabel} style={{ width: 80 }} />
|
|
),
|
|
},
|
|
{
|
|
key: 'memory.high',
|
|
label: 'eventMemoryHigh',
|
|
settingKey: 'tgMemory',
|
|
extra: ({ value, onChange, ariaLabel }) => (
|
|
<InputNumber size="small" min={0} max={100} value={value} onChange={onChange} aria-label={ariaLabel} style={{ width: 80 }} />
|
|
),
|
|
},
|
|
],
|
|
},
|
|
{
|
|
icon: <SafetyOutlined />,
|
|
title: 'eventGroupSecurity',
|
|
events: [
|
|
{ key: 'login.attempt', label: 'eventLoginAttempt', settingKey: '' },
|
|
],
|
|
},
|
|
];
|
|
|
|
interface Props {
|
|
allSetting: AllSetting;
|
|
updateSetting: (patch: Partial<AllSetting>) => void;
|
|
}
|
|
|
|
export function TelegramNotifications({ allSetting, updateSetting }: Props) {
|
|
const events = allSetting.tgEnabledEvents || '';
|
|
const selected = events ? events.split(',').map((s) => s.trim()).filter(Boolean) : [];
|
|
|
|
function toggle(key: string) {
|
|
const next = selected.includes(key)
|
|
? selected.filter((e) => e !== key)
|
|
: [...selected, key];
|
|
updateSetting({ tgEnabledEvents: next.join(',') });
|
|
}
|
|
|
|
function toggleAll(keys: string[]) {
|
|
const allSelected = keys.every((v) => selected.includes(v));
|
|
const next = allSelected
|
|
? selected.filter((v) => !keys.includes(v))
|
|
: [...new Set([...selected, ...keys])];
|
|
updateSetting({ tgEnabledEvents: next.join(',') });
|
|
}
|
|
|
|
return (
|
|
<NotificationLayout>
|
|
{GROUPS.map((group, i) => (
|
|
<NotificationGroup
|
|
key={i}
|
|
config={group}
|
|
selected={selected}
|
|
onToggle={toggle}
|
|
onToggleAll={toggleAll}
|
|
allSetting={allSetting}
|
|
updateSetting={updateSetting}
|
|
/>
|
|
))}
|
|
</NotificationLayout>
|
|
);
|
|
}
|