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.
This commit is contained in:
Grigoriy
2026-10-24 05:26:13 +03:00
committed by GitHub
parent c3037fc716
commit 2f62dadd25
34 changed files with 379 additions and 35 deletions
+2
View File
@@ -39,6 +39,7 @@ export const EXAMPLES: Record<string, unknown> = {
"ldapUserAttr": "",
"ldapUserFilter": "",
"ldapVlessField": "",
"nodeDownThreshold": 1,
"outboundDownThreshold": 1,
"pageSize": 0,
"panelOutbound": "",
@@ -227,6 +228,7 @@ export const EXAMPLES: Record<string, unknown> = {
"ldapUserAttr": "",
"ldapUserFilter": "",
"ldapVlessField": "",
"nodeDownThreshold": 1,
"outboundDownThreshold": 1,
"pageSize": 0,
"panelOutbound": "",
+12
View File
@@ -126,6 +126,11 @@ export const SCHEMAS: Record<string, unknown> = {
"ldapVlessField": {
"type": "string"
},
"nodeDownThreshold": {
"maximum": 100,
"minimum": 1,
"type": "integer"
},
"outboundDownThreshold": {
"maximum": 100,
"minimum": 1,
@@ -613,6 +618,7 @@ export const SCHEMAS: Record<string, unknown> = {
"ldapUserAttr",
"ldapUserFilter",
"ldapVlessField",
"nodeDownThreshold",
"outboundDownThreshold",
"pageSize",
"panelOutbound",
@@ -906,6 +912,11 @@ export const SCHEMAS: Record<string, unknown> = {
"ldapVlessField": {
"type": "string"
},
"nodeDownThreshold": {
"maximum": 100,
"minimum": 1,
"type": "integer"
},
"outboundDownThreshold": {
"maximum": 100,
"minimum": 1,
@@ -1401,6 +1412,7 @@ export const SCHEMAS: Record<string, unknown> = {
"ldapUserAttr",
"ldapUserFilter",
"ldapVlessField",
"nodeDownThreshold",
"outboundDownThreshold",
"pageSize",
"panelOutbound",
+2
View File
@@ -47,6 +47,7 @@ export interface AllSetting {
ldapUserAttr: string;
ldapUserFilter: string;
ldapVlessField: string;
nodeDownThreshold: number;
outboundDownThreshold: number;
pageSize: number;
panelOutbound: string;
@@ -236,6 +237,7 @@ export interface AllSettingView {
ldapUserAttr: string;
ldapUserFilter: string;
ldapVlessField: string;
nodeDownThreshold: number;
outboundDownThreshold: number;
pageSize: number;
panelOutbound: string;
+2
View File
@@ -63,6 +63,7 @@ export const AllSettingSchema = z.object({
ldapUserAttr: z.string(),
ldapUserFilter: z.string(),
ldapVlessField: z.string(),
nodeDownThreshold: z.number().int().min(1).max(100),
outboundDownThreshold: z.number().int().min(1).max(100),
pageSize: z.number().int().min(0).max(1000),
panelOutbound: z.string(),
@@ -253,6 +254,7 @@ export const AllSettingViewSchema = z.object({
ldapUserAttr: z.string(),
ldapUserFilter: z.string(),
ldapVlessField: z.string(),
nodeDownThreshold: z.number().int().min(1).max(100),
outboundDownThreshold: z.number().int().min(1).max(100),
pageSize: z.number().int().min(0).max(1000),
panelOutbound: z.string(),