Files
MikrotikManager/backend/src/services/alert-engine/confirm-stability.ts
T
DenozordecandCursor ec43591a99
Docker images / prepare-release (push) Successful in 12s
Docker images / backend-test (push) Successful in 4m9s
Docker images / frontend-image (push) Successful in 4m28s
Docker images / updater-image (push) Successful in 58s
Docker images / backend-image (push) Successful in 2m49s
Docker images / notify-webhook (push) Skipped
Docker images / publish-release (push) Successful in 11s
feat(db): перевести хранилище с SQLite на PostgreSQL
При старте backend накатывает схему PostgreSQL 18 и, если база пустая, один раз импортирует mikrotik.db с тома. Повторный старт не копирует данные. Бэкап в UI идёт через pg_dump.

Co-authored-by: Cursor <[email protected]>
2026-09-08 01:36:48 +07:00

81 lines
2.7 KiB
TypeScript

import { eq } from "drizzle-orm"
import { db } from "../../db/index.js"
import { alertEngineConfirmPending } from "../../db/schema.js"
import type { ApiAlertRule } from "../alerts-service.js"
import type { RuleEvalHit } from "./types.js"
export async function deleteConfirmPending(ruleId: string) {
await db.delete(alertEngineConfirmPending).where(eq(alertEngineConfirmPending.ruleId, ruleId))
}
/**
* Обновляет состояние ожидания стабильности и возвращает, можно ли отправлять уведомление в этом тике.
* При отсутствии срабатывания (`hit == null`) ожидание сбрасывается.
*/
export async function stabilityReadyForSend(
ruleId: string,
hit: RuleEvalHit | null,
delaySec: number | null | undefined,
): Promise<boolean> {
if (!hit) {
await deleteConfirmPending(ruleId)
return false
}
const sec = delaySec != null && delaySec > 0 ? Math.floor(delaySec) : 0
if (sec <= 0) {
await deleteConfirmPending(ruleId)
return true
}
const now = Date.now()
const nowIso = new Date().toISOString()
const existing = (await db
.select()
.from(alertEngineConfirmPending)
.where(eq(alertEngineConfirmPending.ruleId, ruleId))
.limit(1))[0]
if (!existing || existing.payloadHash !== hit.payloadHash) {
if (existing) {
await db.update(alertEngineConfirmPending)
.set({ payloadHash: hit.payloadHash, sinceAt: nowIso })
.where(eq(alertEngineConfirmPending.ruleId, ruleId))
} else {
await db.insert(alertEngineConfirmPending).values({ ruleId, payloadHash: hit.payloadHash, sinceAt: nowIso })
}
return false
}
const t = Date.parse(existing.sinceAt)
if (!Number.isFinite(t)) return false
return now - t >= sec * 1000
}
/** После успешной отправки — сбросить таймер стабильности для следующего цикла. */
export async function clearConfirmPendingAfterSend(ruleIds: string[]) {
for (const id of ruleIds) await deleteConfirmPending(id)
}
export async function computeStabilityReadyMap(
rules: ApiAlertRule[],
hitByRule: Map<string, RuleEvalHit | null>,
): Promise<Map<string, boolean>> {
const m = new Map<string, boolean>()
for (const rule of rules) {
if (!rule.enabled) {
await deleteConfirmPending(rule.id)
m.set(rule.id, false)
continue
}
const hit = hitByRule.get(rule.id) ?? null
const delaySec =
hit?.transition === "recovery"
? rule.recoveryMode === "conditional"
? rule.recoveryStabilitySec
: 0
: rule.confirmStabilitySec
m.set(rule.id, await stabilityReadyForSend(rule.id, hit, delaySec))
}
return m
}