import { z } from "zod" export const alertTypeSchema = z.enum([ "gre-tunnel", "bgp-peer", "bgp-prefix", "gre-client", "server", "rtt", "loss", "traffic", ]) export const alertSeveritySchema = z.enum(["critical", "warning", "info"]) export const alertCooldownSchema = z.enum(["1м", "5м", "15м", "1ч", "4ч", "24ч"]) export const recoveryModeSchema = z.enum(["always", "never", "conditional"]) export const combineModeSchema = z.enum(["any", "all"]) export const alertGroupSchema = z.object({ id: z.string().min(1), name: z.string().min(1), combineMode: combineModeSchema, enabled: z.boolean(), cooldownOverride: z.union([alertCooldownSchema, z.null()]), }) export const alertRuleSchema = z .object({ id: z.string().min(1), name: z.string().min(1), type: alertTypeSchema, target: z.string().optional(), targets: z.array(z.string().min(1)).optional(), groupId: z.string().nullable().optional(), condition: z.string().optional(), conditions: z.array(z.string().min(1)).optional(), severity: alertSeveritySchema, enabled: z.boolean(), cooldown: alertCooldownSchema, confirmStabilitySec: z.union([z.number().int().min(0).max(86400), z.null()]).optional(), recoveryMode: recoveryModeSchema.optional(), recoveryStabilitySec: z.union([z.number().int().min(0).max(86400), z.null()]).optional(), chatId: z.string(), }) .refine((r) => (r.targets != null && r.targets.length > 0) || Boolean(r.target?.trim()), { message: "Нужен хотя бы один объект: targets или target", path: ["targets"], }) .refine((r) => (r.conditions != null && r.conditions.length > 0) || Boolean(r.condition?.trim()), { message: "Нужно хотя бы одно условие: conditions или condition", path: ["conditions"], }) export const putRulesSchema = z.object({ rules: z.array(alertRuleSchema), groups: z.array(alertGroupSchema).optional(), }) export const putTelegramSchema = z.object({ token: z.union([z.string(), z.null()]).optional(), chatId: z.string().optional(), messageThreadId: z.union([z.number().int().positive().max(2147483647), z.null()]).optional(), }) export const rulePreviewTelegramSchema = z.object({ name: z.string().min(1), targets: z.array(z.string().min(1)).min(1), conditionLine: z.string().min(1), severity: alertSeveritySchema, cooldown: alertCooldownSchema, }) export const testTelegramSchema = z.object({ token: z.string().optional(), chatId: z.string().optional(), messageThreadId: z.number().int().positive().max(2147483647).optional(), rulePreview: rulePreviewTelegramSchema.optional(), }) export type AlertGroup = z.infer export type AlertRule = z.infer export type PutRulesBody = z.infer export type PutTelegramBody = z.infer export type RulePreviewTelegram = z.infer export type TestTelegramBody = z.infer // Public response schemas: configured credentials do not prove Telegram connectivity/delivery. const alertTimestampSchema = z.string().min(1).refine(value => Number.isFinite(Date.parse(value)), "Некорректное время оповещения") export const telegramPublicSchema = z.object({ chatId:z.string(),tokenConfigured:z.boolean(),connected:z.boolean(),messageThreadId:z.number().int().positive().max(2147483647).nullable(), }).strict().refine(value => value.connected === (value.tokenConfigured && Boolean(value.chatId.trim())), "Противоречивое состояние параметров Telegram") export const alertRuleResponseSchema = z.object({ id:z.string().min(1),name:z.string().min(1),type:alertTypeSchema,target:z.string(),targets:z.array(z.string().min(1)),groupId:z.string().nullable(),condition:z.string(),conditions:z.array(z.string().min(1)),severity:alertSeveritySchema,enabled:z.boolean(),cooldown:alertCooldownSchema, confirmStabilitySec:z.number().int().min(0).max(86400).nullable(),recoveryMode:recoveryModeSchema,recoveryStabilitySec:z.number().int().min(0).max(86400).nullable(),chatId:z.string(),lastFiredAt:alertTimestampSchema.nullable(), }).strict() export const alertHistoryResponseSchema = z.object({id:z.string().min(1),ruleName:z.string(),severity:alertSeveritySchema,message:z.string(),sent:z.boolean(),firedAt:alertTimestampSchema,source:z.enum(["db","live"])}).strict() export const alertTargetIssueSchema = z.object({ kind:z.enum(["server","traffic","probe"]),target:z.string().min(1),reason:z.enum(["ambiguous","disabled-object","disabled-source"]), }).strict() export type AlertTargetIssue=z.output export const alertsMetaResponseSchema = z.object({ servers:z.array(z.string()),greClients:z.array(z.string()),probeTargets:z.array(z.string()),rttLossTargets:z.array(z.string()),trafficServers:z.array(z.string()), serversDetail:z.array(z.object({name:z.string(),host:z.string(),site:z.string(),country:z.string(),type:z.string()}).strict()), targetIssues:z.array(alertTargetIssueSchema).optional(), }).strict() const alertsConfigResponseShape = {groups:z.array(alertGroupSchema.strict()),rules:z.array(alertRuleResponseSchema)} function validConfig(value:{groups:{id:string}[];rules:{id:string;groupId:string|null}[]}):boolean { const groups=new Set(value.groups.map(row=>row.id)),rules=new Set(value.rules.map(row=>row.id)) return groups.size===value.groups.length&&rules.size===value.rules.length&&value.rules.every(row=>row.groupId===null||groups.has(row.groupId)) } export const alertsRevisionSchema=z.string().regex(/^[a-f0-9]{64}$/) export const alertsResponseSchema = z.object({revision:alertsRevisionSchema,telegram:telegramPublicSchema,...alertsConfigResponseShape,history:z.array(alertHistoryResponseSchema),meta:alertsMetaResponseSchema}).strict().refine(validConfig,"Повторяющиеся ID или неизвестная группа правила") export const alertsRulesReceiptSchema = z.object({ok:z.literal(true),revision:alertsRevisionSchema,...alertsConfigResponseShape}).strict().refine(validConfig,"Повторяющиеся ID или неизвестная группа правила") export const telegramSettingsReceiptSchema = z.object({ok:z.literal(true),telegram:telegramPublicSchema}).strict() export const telegramTestReceiptSchema = z.object({ok:z.literal(true)}).strict() export type AlertsResponse = z.output /** Normalize the stored rule representation without altering OR/AND or cooldown policies. */ export function normalizeAlertsConfig(input:PutRulesBody){ const parsed=putRulesSchema.strict().safeParse(input) if(!parsed.success)throw Error("Проверьте правила и группы оповещений") const groups=parsed.data.groups?.map(group=>({...group,name:group.name.trim()||group.id})) const groupIds=groups===undefined?undefined:new Set(groups.map(group=>group.id)) const ids=new Set() if(groups!==undefined&&groupIds!.size!==groups.length)throw Error("Повторяющиеся ID групп") const rules=parsed.data.rules.map(rule=>{ if(ids.has(rule.id))throw Error("Повторяющиеся ID правил") ids.add(rule.id) const targets=(rule.targets?.length?rule.targets:[rule.target??""]).map(value=>value.trim()).filter(Boolean) const conditions=(rule.conditions?.length?rule.conditions:[rule.condition??""]).map(value=>value.trim()).filter(Boolean) if(!targets.length||!conditions.length)throw Error("Нужны непустые объекты и условия правила") const groupId=rule.groupId?.trim()||null if(groupId!==null&&groupIds!==undefined&&!groupIds.has(groupId))throw Error("Неизвестная группа правила") const target=targets.length<=2?targets.join(", "):targets[0]+", "+targets[1]+" +"+(targets.length-2) const condition=conditions.length===1?conditions[0]!:conditions.length<=3?conditions.join(" · "):conditions[0]+" · "+conditions[1]+" · +"+(conditions.length-2) return {...rule,target,targets,condition,conditions,groupId,confirmStabilitySec:rule.confirmStabilitySec||null,recoveryMode:rule.recoveryMode??"always",recoveryStabilitySec:rule.recoveryStabilitySec||null} }) return {...(groups!==undefined?{groups}:{}),rules} } const outboxCount=z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER) export const alertsOutboxOverviewSchema=z.object({counts:z.object({total:outboxCount,pending:outboxCount,processing:outboxCount,unknown:outboxCount,sent:outboxCount,failed:outboxCount,unrecognized:outboxCount}).strict(),items:z.array(z.object({id:z.string().min(1),resolutionVersion:z.number().int().nonnegative().max(2147483647),status:z.enum(["processing","unknown"]),ruleName:z.string().nullable(),createdAt:alertTimestampSchema,attemptedAt:alertTimestampSchema}).strict()).max(20),itemsLimit:z.literal(20)}).strict().refine(value=>value.counts.total===value.counts.pending+value.counts.processing+value.counts.unknown+value.counts.sent+value.counts.failed+value.counts.unrecognized&&value.items.length===Math.min(20,value.counts.processing+value.counts.unknown)&&value.items.filter(item=>item.status==="processing").length<=value.counts.processing&&value.items.filter(item=>item.status==="unknown").length<=value.counts.unknown&&new Set(value.items.map(item=>item.id)).size===value.items.length,"Неполное состояние очереди") export type AlertsOutboxOverview=z.output export const alertsOutboxRetrySchema=z.object({expectedVersion:z.number().int().nonnegative().max(2147483646),acknowledgedDuplicateRisk:z.literal(true)}).strict() export const alertsOutboxRetryReceiptSchema=z.object({ok:z.literal(true),id:z.string().min(1),status:z.literal("pending"),resolutionVersion:z.number().int().min(1).max(2147483647)}).strict()