Files
3x-ui/frontend/src/schemas/setting.ts
T
Grigoriy 2f62dadd25 feat(notifications): add a failed-check threshold for node.down alerts (#6768)
One lost heartbeat on a lossy link flipped a node offline and sent node.down,
and the next probe sent node.up, so a flaky node flooded Telegram, email and
Discord with alert pairs.

A new nodeDownThreshold setting (default 1, today's behaviour) works like
outboundDownThreshold: node.down fires once the failed-check streak reaches it,
once per outage, and node.up only after a node.down was sent. The announcement
is persisted in nodes.down_notified, so a restart inside the threshold window
neither drops the outage's node.down nor re-announces nodes already reported
offline; nodes offline at upgrade are seeded as announced.
2026-10-24 04:26:13 +02:00

201 lines
8.8 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 SubProfileModeSchema = z.enum(['none', 'builtin', 'custom']);
export type SubProfileMode = z.infer<typeof SubProfileModeSchema>;
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(),
realityScanCandidates: z.string().optional(),
ipLimitAllowlist: 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(),
subShowIdentityOnAllLinks: z.boolean().optional(),
subInfoNodeEnable: z.boolean().optional(),
subCalendarExpireInclusive: z.boolean().optional(),
subExpiredTemplate: z.string().optional(),
subTrafficDepletedTemplate: 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(),
nodeDownThreshold: z.number().int().min(1).max(100).optional(),
tgLang: z.string().optional(),
twoFactorEnable: z.boolean().optional(),
twoFactorToken: z.string().optional(),
xrayTemplateConfig: z.string().optional(),
happLinkEnable: z.boolean().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(),
subProfileMode: SubProfileModeSchema.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(),
externalSubUserAgent: z.string().max(512).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(),
subClashRemoteDNS: z.boolean().optional(),
subJsonMux: z.string().optional(),
subJsonRules: z.string().optional(),
subJsonRoutingRules: z.string().optional(),
subJsonDns: z.string().optional(),
subJsonFinalMask: z.string().optional(),
subJsonObservatory: z.string().optional(),
subHideSettings: z.boolean().optional(),
subHappAutoDetect: z.boolean().optional(),
subHappProviderId: z.string().optional(),
subHappNewUrl: z.string().optional(),
subHappFallbackUrl: z.string().optional(),
subHappSubInfoColor: z.string().optional(),
subHappSubInfoText: z.string().optional(),
subHappSubInfoButtonText: z.string().optional(),
subHappSubInfoButtonLink: z.string().optional(),
subHappSubExpire: z.boolean().optional(),
subHappSubExpireButtonLink: z.string().optional(),
subHappNotificationExpire: z.boolean().optional(),
subHappNoLimit: z.boolean().optional(),
subHappAlwaysHwid: z.boolean().optional(),
subHappTunMode: z.string().optional(),
subHappTunType: z.string().optional(),
subHappExcludeRoutes: z.string().optional(),
subHappExcludeApns: z.boolean().optional(),
subHappColorProfile: z.string().optional(),
subHappPingType: z.string().optional(),
subHappAutoConnect: z.boolean().optional(),
subHappAutoConnectType: z.string().optional(),
subHappPerAppMode: z.string().optional(),
subHappPerAppList: z.string().optional(),
subHappLocalProxyAuth: z.string().optional(),
subIncyAppAutoDetect: z.boolean().optional(),
subIncyProfileDescription: z.string().optional(),
subIncySortOrder: z.string().optional(),
subIncySupportEmail: z.string().optional(),
subIncyAnnounceUrl: z.string().optional(),
subIncyPremiumUrl: z.string().optional(),
subIncyBannerText: z.string().optional(),
subIncyBannerButtonText: z.string().optional(),
subIncyBannerButtonUrl: z.string().optional(),
subIncyBannerBgColor: z.string().optional(),
subIncyBannerButtonColor: z.string().optional(),
subIncyHideUrl: z.string().optional(),
subIncyHideCheck: z.string().optional(),
subIncyNoLimitEnabled: z.string().optional(),
subIncyPerAppEnable: z.string().optional(),
subIncyPerAppMode: z.string().optional(),
subIncyPerAppList: z.string().optional(),
subIncyFragmentationEnable: z.string().optional(),
subIncyFragmentLength: z.string().optional(),
subIncyFragmentInterval: z.string().optional(),
subIncyFragmentPackets: z.string().optional(),
subIncyNoisesEnable: z.string().optional(),
subIncyNoisesType: z.string().optional(),
subIncyNoisesPacket: z.string().optional(),
subIncyNoisesDelay: z.string().optional(),
subIncyResolveEnable: z.string().optional(),
subIncyResolveDnsDomain: z.string().optional(),
subIncyResolveDnsIp: z.string().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(),
hasDiscordBotToken: z.boolean().optional(),
discordBotEnable: z.boolean().optional(),
discordBotToken: z.string().optional(),
discordChannelId: z.string().optional(),
discordAdminIds: z.string().optional(),
discordRunTime: z.string().optional(),
discordBotBackup: z.boolean().optional(),
discordCpu: z.number().int().min(0).max(100).optional(),
discordMemory: z.number().int().min(0).max(100).optional(),
discordLang: z.string().optional(),
discordEnabledEvents: z.string().optional(),
})
.loose();
export type AllSettingInput = z.infer<typeof AllSettingSchema>;
// Existing installations can contain regex values saved before the backend
// enforced its 2,048-character limit. Accept those values when reading so the
// settings page can display and let users correct them, while keeping the
// stricter schema above for outgoing updates.
export const AllSettingResponseSchema = AllSettingSchema.extend({
subJsonUserAgentRegex: z.string().optional(),
subClashUserAgentRegex: z.string().optional(),
});
export const FactoryDefaultsSchema = z.record(z.string(), z.string());
export type FactoryDefaults = z.infer<typeof FactoryDefaultsSchema>;