mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-31 02:05:00 +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>
105 lines
4.2 KiB
TypeScript
105 lines
4.2 KiB
TypeScript
import { z } from 'zod';
|
|
|
|
const port = z.number().int().min(1).max(65535);
|
|
const nonNegativeInt = z.number().int().min(0);
|
|
const absolutePath = z.string().regex(/^\//, 'pages.settings.validation.pathLeadingSlash');
|
|
|
|
export const AllSettingSchema = z.object({
|
|
webListen: z.string().optional(),
|
|
webDomain: z.string().optional(),
|
|
webPort: port.optional(),
|
|
webCertFile: z.string().optional(),
|
|
webKeyFile: z.string().optional(),
|
|
webBasePath: absolutePath.optional(),
|
|
sessionMaxAge: z.number().int().min(1).max(525600).optional(),
|
|
trustedProxyCIDRs: z.string().optional(),
|
|
panelOutbound: z.string().optional(),
|
|
pageSize: z.number().int().min(0).max(1000).optional(),
|
|
expireDiff: nonNegativeInt.optional(),
|
|
trafficDiff: nonNegativeInt.max(100).optional(),
|
|
remarkTemplate: z.string().optional(),
|
|
datepicker: z.enum(['gregorian', 'jalalian']).optional(),
|
|
tgBotEnable: z.boolean().optional(),
|
|
tgBotToken: z.string().optional(),
|
|
tgBotProxy: z.string().optional(),
|
|
tgBotAPIServer: z.string().optional(),
|
|
tgBotChatId: z.string().optional(),
|
|
tgRunTime: z.string().optional(),
|
|
tgBotBackup: z.boolean().optional(),
|
|
tgCpu: z.number().int().min(0).max(100).optional(),
|
|
outboundDownThreshold: z.number().int().min(1).max(100).optional(),
|
|
tgLang: z.string().optional(),
|
|
twoFactorEnable: z.boolean().optional(),
|
|
twoFactorToken: z.string().optional(),
|
|
xrayTemplateConfig: z.string().optional(),
|
|
subEnable: z.boolean().optional(),
|
|
subJsonEnable: z.boolean().optional(),
|
|
subJsonAutoDetect: z.boolean().optional(),
|
|
subJsonAlwaysArray: z.boolean().optional(),
|
|
subJsonUserAgentRegex: z.string().max(2048).optional(),
|
|
subClashAutoDetect: z.boolean().optional(),
|
|
subClashUserAgentRegex: z.string().max(2048).optional(),
|
|
subTitle: z.string().optional(),
|
|
subSupportUrl: z.string().optional(),
|
|
subProfileUrl: z.string().optional(),
|
|
subAnnounce: z.string().optional(),
|
|
subEnableRouting: z.boolean().optional(),
|
|
subRoutingRules: z.string().optional(),
|
|
subIncyEnableRouting: z.boolean().optional(),
|
|
subIncyRoutingRules: z.string().optional(),
|
|
subListen: z.string().optional(),
|
|
subPort: port.optional(),
|
|
subPath: absolutePath.optional(),
|
|
subJsonPath: absolutePath.optional(),
|
|
subClashEnable: z.boolean().optional(),
|
|
subClashPath: absolutePath.optional(),
|
|
subDomain: z.string().optional(),
|
|
externalTrafficInformEnable: z.boolean().optional(),
|
|
externalTrafficInformURI: z.string().optional(),
|
|
restartXrayOnClientDisable: z.boolean().optional(),
|
|
subCertFile: z.string().optional(),
|
|
subKeyFile: z.string().optional(),
|
|
subUpdates: z.number().int().min(0).max(525600).optional(),
|
|
subEncrypt: z.boolean().optional(),
|
|
subURI: z.string().optional(),
|
|
subJsonURI: z.string().optional(),
|
|
subClashURI: z.string().optional(),
|
|
subClashEnableRouting: z.boolean().optional(),
|
|
subClashRules: z.string().optional(),
|
|
subJsonMux: z.string().optional(),
|
|
subJsonRules: z.string().optional(),
|
|
subJsonFinalMask: z.string().optional(),
|
|
subHideSettings: z.boolean().optional(),
|
|
timeLocation: z.string().optional(),
|
|
ldapEnable: z.boolean().optional(),
|
|
ldapHost: z.string().optional(),
|
|
ldapPort: port.optional(),
|
|
ldapUseTLS: z.boolean().optional(),
|
|
ldapInsecureSkipVerify: z.boolean().optional(),
|
|
ldapBindDN: z.string().optional(),
|
|
ldapPassword: z.string().optional(),
|
|
ldapBaseDN: z.string().optional(),
|
|
ldapUserFilter: z.string().optional(),
|
|
ldapUserAttr: z.string().optional(),
|
|
ldapVlessField: z.string().optional(),
|
|
ldapSyncCron: z.string().optional(),
|
|
ldapFlagField: z.string().optional(),
|
|
ldapTruthyValues: z.string().optional(),
|
|
ldapInvertFlag: z.boolean().optional(),
|
|
ldapInboundTags: z.string().optional(),
|
|
ldapAutoCreate: z.boolean().optional(),
|
|
ldapAutoDelete: z.boolean().optional(),
|
|
ldapDefaultTotalGB: nonNegativeInt.optional(),
|
|
ldapDefaultExpiryDays: nonNegativeInt.optional(),
|
|
ldapDefaultLimitIP: nonNegativeInt.optional(),
|
|
hasTgBotToken: z.boolean().optional(),
|
|
hasTwoFactorToken: z.boolean().optional(),
|
|
hasLdapPassword: z.boolean().optional(),
|
|
hasApiToken: z.boolean().optional(),
|
|
hasWarpSecret: z.boolean().optional(),
|
|
hasNordSecret: z.boolean().optional(),
|
|
hasSmtpPassword: z.boolean().optional(),
|
|
}).loose();
|
|
|
|
export type AllSettingInput = z.infer<typeof AllSettingSchema>;
|