Files
3x-ui/frontend/src/models/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

230 lines
6.4 KiB
TypeScript

import { ObjectUtil } from '@/utils';
import type { SubProfileMode } from '@/schemas/setting';
export class AllSetting {
webListen = '';
webDomain = '';
webPort = 2053;
webCertFile = '';
webKeyFile = '';
webBasePath = '/';
sessionMaxAge = 360;
trustedProxyCIDRs = '127.0.0.1/32,::1/128';
realityScanCandidates =
'www.cloudflare.com:443,www.microsoft.com:443,www.amazon.com:443,aws.amazon.com:443,www.samsung.com:443,www.nvidia.com:443,www.amd.com:443,www.intel.com:443,www.sony.com:443,dl.google.com:443';
ipLimitAllowlist = '';
panelOutbound = '';
pageSize = 25;
expireDiff = 0;
trafficDiff = 0;
remarkTemplate = '{{INBOUND}}-{{EMAIL}}|📊{{TRAFFIC_LEFT}}|⏳{{DAYS_LEFT}}D';
subShowIdentityOnAllLinks = false;
subInfoNodeEnable = false;
subCalendarExpireInclusive = false;
subExpiredTemplate = '⛔ {{EMAIL}} | Expired: {{EXPIRE_DATE}}';
subTrafficDepletedTemplate =
'🚫 {{EMAIL}} | Traffic Depleted | {{TRAFFIC_USED}}/{{TRAFFIC_TOTAL}}';
datepicker: 'gregorian' | 'jalalian' = 'gregorian';
tgBotEnable = false;
tgBotToken = '';
tgBotAPIServer = '';
tgBotChatId = '';
tgRunTime = '@daily';
tgBotBackup = false;
tgCpu = 80;
tgMemory = 80;
tgLang = 'en-US';
twoFactorEnable = false;
twoFactorToken = '';
xrayTemplateConfig = '';
happLinkEnable = false;
subEnable = true;
subJsonEnable = false;
subJsonAutoDetect = false;
subJsonAlwaysArray = false;
subJsonUserAgentRegex = '';
subClashAutoDetect = false;
subClashUserAgentRegex = '';
subTitle = '';
subSupportUrl = '';
subProfileMode: SubProfileMode = 'none';
subProfileUrl = '';
subAnnounce = '';
subEnableRouting = false;
subRoutingRules = '';
subIncyEnableRouting = false;
subIncyRoutingRules = '';
subListen = '';
subPort = 2096;
subPath = '/sub/';
subJsonPath = '/json/';
subClashEnable = false;
subClashPath = '/clash/';
subDomain = '';
externalTrafficInformEnable = false;
externalTrafficInformURI = '';
restartXrayOnClientDisable = true;
subCertFile = '';
subKeyFile = '';
externalSubUserAgent = 'v2rayNG/1.8.5';
subUpdates = 12;
subEncrypt = true;
subURI = '';
subJsonURI = '';
subClashURI = '';
subClashEnableRouting = false;
subClashRules = '';
subClashRemoteDNS = false;
subJsonMux = '';
subJsonRules = '';
subJsonRoutingRules = '';
subJsonDns = '';
subJsonFinalMask = '';
subJsonObservatory = '';
subThemeDir = '';
subHideSettings = false;
subHappAutoDetect = false;
subHappProviderId = '';
subHappNewUrl = '';
subHappFallbackUrl = '';
subHappSubInfoColor = 'blue';
subHappSubInfoText = '';
subHappSubInfoButtonText = '';
subHappSubInfoButtonLink = '';
subHappSubExpire = false;
subHappSubExpireButtonLink = '';
subHappNotificationExpire = false;
subHappNoLimit = false;
subHappAlwaysHwid = false;
subHappTunMode = '';
subHappTunType = '';
subHappExcludeRoutes = '';
subHappExcludeApns = false;
subHappColorProfile = '';
subHappPingType = '';
subHappAutoConnect = false;
subHappAutoConnectType = 'lowestdelay';
subHappPerAppMode = 'off';
subHappPerAppList = '';
subHappLocalProxyAuth = 'auto';
subIncyAppAutoDetect = false;
subIncyProfileDescription = '';
subIncySortOrder = '';
subIncySupportEmail = '';
subIncyAnnounceUrl = '';
subIncyPremiumUrl = '';
subIncyBannerText = '';
subIncyBannerButtonText = '';
subIncyBannerButtonUrl = '';
subIncyBannerBgColor = '';
subIncyBannerButtonColor = '';
subIncyHideUrl = '';
subIncyHideCheck = '';
subIncyNoLimitEnabled = '';
subIncyPerAppEnable = '';
subIncyPerAppMode = '';
subIncyPerAppList = '';
subIncyFragmentationEnable = '';
subIncyFragmentLength = '';
subIncyFragmentInterval = '';
subIncyFragmentPackets = '';
subIncyNoisesEnable = '';
subIncyNoisesType = '';
subIncyNoisesPacket = '';
subIncyNoisesDelay = '';
subIncyResolveEnable = '';
subIncyResolveDnsDomain = '';
subIncyResolveDnsIp = '';
timeLocation = 'Local';
ldapEnable = false;
ldapHost = '';
ldapPort = 389;
ldapUseTLS = false;
ldapInsecureSkipVerify = false;
ldapBindDN = '';
ldapPassword = '';
ldapBaseDN = '';
ldapUserFilter = '(objectClass=person)';
ldapUserAttr = 'mail';
ldapVlessField = 'vless_enabled';
ldapSyncCron = '@every 1m';
ldapFlagField = '';
ldapTruthyValues = 'true,1,yes,on';
ldapInvertFlag = false;
ldapInboundTags = '';
ldapAutoCreate = false;
ldapAutoDelete = false;
ldapDefaultTotalGB = 0;
ldapDefaultExpiryDays = 0;
ldapDefaultLimitIP = 0;
tgEnabledEvents = 'login.attempt,cpu.high';
smtpEnable = false;
smtpHost = '';
smtpPort = 587;
smtpUsername = '';
smtpPassword = '';
smtpFrom = '';
smtpFromName = '';
smtpTo = '';
smtpEncryptionType = 'starttls';
smtpEnabledEvents = 'login.attempt,cpu.high';
smtpCpu = 80;
smtpMemory = 80;
outboundDownThreshold = 3;
nodeDownThreshold = 1;
hasTgBotToken = false;
hasTwoFactorToken = false;
hasLdapPassword = false;
hasApiToken = false;
hasWarpSecret = false;
hasNordSecret = false;
hasSmtpPassword = false;
clearTgBotToken = false;
clearLdapPassword = false;
clearSmtpPassword = false;
discordBotEnable = false;
discordBotToken = '';
discordChannelId = '';
discordAdminIds = '';
discordRunTime = '@daily';
discordBotBackup = false;
discordCpu = 80;
discordMemory = 80;
discordLang = 'en-US';
discordEnabledEvents = 'login.attempt,cpu.high';
hasDiscordBotToken = false;
clearDiscordBotToken = false;
constructor(data?: unknown) {
if (data != null) {
ObjectUtil.cloneProps(this, data);
}
// Legacy settings with a custom URL retain it until an explicit mode is saved.
if (
typeof data === 'object' &&
data !== null &&
(!('subProfileMode' in data) || data.subProfileMode === undefined) &&
this.subProfileUrl.trim() !== ''
) {
this.subProfileMode = 'custom';
}
const cpu = Math.round(Number(this.tgCpu));
this.tgCpu = Number.isFinite(cpu) ? Math.min(100, Math.max(0, cpu)) : 80;
const threshold = Math.round(Number(this.outboundDownThreshold));
this.outboundDownThreshold = Number.isFinite(threshold)
? Math.min(100, Math.max(1, threshold))
: 3;
const nodeThreshold = Math.round(Number(this.nodeDownThreshold));
this.nodeDownThreshold = Number.isFinite(nodeThreshold)
? Math.min(100, Math.max(1, nodeThreshold))
: 1;
}
equals(other: AllSetting): boolean {
return ObjectUtil.equals(this, other);
}
}