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
При старте backend накатывает схему PostgreSQL 18 и, если база пустая, один раз импортирует mikrotik.db с тома. Повторный старт не копирует данные. Бэкап в UI идёт через pg_dump. Co-authored-by: Cursor <[email protected]>
135 lines
4.0 KiB
TypeScript
135 lines
4.0 KiB
TypeScript
import { and, asc, eq, lte } from "drizzle-orm"
|
|
import { db } from "../../db/index.js"
|
|
import { alertOutbox } from "../../db/schema.js"
|
|
import { appendAlertHistory, sendTelegramAlertMessage } from "../alerts-service.js"
|
|
|
|
export type AlertOutboxPayload = {
|
|
text: string
|
|
chatId?: string
|
|
history: {
|
|
id: string
|
|
ruleId?: string | null
|
|
groupId?: string | null
|
|
ruleName: string
|
|
severity: "critical" | "warning" | "info"
|
|
message: string
|
|
firedAt: string
|
|
}
|
|
}
|
|
|
|
function safeParsePayload(raw: unknown): AlertOutboxPayload | null {
|
|
if (raw && typeof raw === "object") return raw as AlertOutboxPayload
|
|
if (typeof raw !== "string") return null
|
|
try {
|
|
return JSON.parse(raw) as AlertOutboxPayload
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
export async function enqueueTelegramOutbox(input: {
|
|
id: string
|
|
dedupeKey: string
|
|
payload: AlertOutboxPayload
|
|
maxRetries?: number
|
|
}): Promise<boolean> {
|
|
const existing = (await db
|
|
.select()
|
|
.from(alertOutbox)
|
|
.where(eq(alertOutbox.dedupeKey, input.dedupeKey))
|
|
.limit(1))[0]
|
|
if (existing && (existing.status === "pending" || existing.status === "sent")) return false
|
|
const nowIso = new Date().toISOString()
|
|
await db.insert(alertOutbox)
|
|
.values({
|
|
id: input.id,
|
|
dedupeKey: input.dedupeKey,
|
|
channel: "telegram",
|
|
status: "pending",
|
|
retryCount: 0,
|
|
maxRetries: Math.max(1, Math.floor(input.maxRetries ?? 3)),
|
|
nextAttemptAt: nowIso,
|
|
payloadJson: input.payload,
|
|
})
|
|
return true
|
|
}
|
|
|
|
export function dispatchPendingOutbox(limit = 25): Promise<{ sent: number; failed: number; errors: string[] }> {
|
|
return (async () => {
|
|
const nowIso = new Date().toISOString()
|
|
const rows = await db
|
|
.select()
|
|
.from(alertOutbox)
|
|
.where(and(eq(alertOutbox.status, "pending"), lte(alertOutbox.nextAttemptAt, nowIso)))
|
|
.orderBy(asc(alertOutbox.createdAt))
|
|
.limit(Math.max(1, limit))
|
|
|
|
let sent = 0
|
|
let failed = 0
|
|
const errors: string[] = []
|
|
|
|
for (const row of rows) {
|
|
const payload = safeParsePayload(row.payloadJson)
|
|
if (!payload) {
|
|
await db.update(alertOutbox)
|
|
.set({
|
|
status: "failed",
|
|
lastError: "invalid payload_json",
|
|
})
|
|
.where(eq(alertOutbox.id, row.id))
|
|
failed += 1
|
|
continue
|
|
}
|
|
const send = await sendTelegramAlertMessage({ text: payload.text, chatId: payload.chatId })
|
|
if (send.ok) {
|
|
await db.update(alertOutbox)
|
|
.set({
|
|
status: "sent",
|
|
sentAt: new Date().toISOString(),
|
|
lastError: null,
|
|
})
|
|
.where(eq(alertOutbox.id, row.id))
|
|
await appendAlertHistory({
|
|
id: payload.history.id,
|
|
ruleId: payload.history.ruleId ?? null,
|
|
groupId: payload.history.groupId ?? null,
|
|
ruleName: payload.history.ruleName,
|
|
severity: payload.history.severity,
|
|
message: payload.history.message,
|
|
sentOk: true,
|
|
firedAt: payload.history.firedAt,
|
|
})
|
|
sent += 1
|
|
continue
|
|
}
|
|
const nextRetry = row.retryCount + 1
|
|
const exhausted = nextRetry >= row.maxRetries
|
|
await db.update(alertOutbox)
|
|
.set({
|
|
retryCount: nextRetry,
|
|
status: exhausted ? "failed" : "pending",
|
|
nextAttemptAt: new Date(Date.now() + Math.min(300_000, nextRetry * 15_000)).toISOString(),
|
|
lastError: send.error,
|
|
})
|
|
.where(eq(alertOutbox.id, row.id))
|
|
if (exhausted) {
|
|
await appendAlertHistory({
|
|
id: payload.history.id,
|
|
ruleId: payload.history.ruleId ?? null,
|
|
groupId: payload.history.groupId ?? null,
|
|
ruleName: payload.history.ruleName,
|
|
severity: payload.history.severity,
|
|
message: payload.history.message,
|
|
sentOk: false,
|
|
firedAt: payload.history.firedAt,
|
|
})
|
|
failed += 1
|
|
} else {
|
|
errors.push(`outbox ${row.id}: ${send.error}`)
|
|
}
|
|
}
|
|
|
|
return { sent, failed, errors }
|
|
})()
|
|
}
|