import { and, desc, eq, isNotNull, isNull } from 'drizzle-orm'; import type { Db } from '../db/client.ts'; import { parserRuns, settings, users } from '../db/schema.ts'; import { isQuietHours, lastQuietEnd } from '../parser/quiet-hours.ts'; import { loadParserSettings } from '../parser/settings.ts'; import { enqueueRaw } from '../telegram/outbox.ts'; // Сторож парсера: если парсер выключен или давно не запускался, суперадминам приходит сообщение // в Telegram; пока проблема не ушла — напоминание раз в несколько часов, после — «снова работает». // 20.10 парсер простоял почти двое суток выключенным, и это заметили только по пропавшему объекту. /** Сколько минут без нового прохода считается «стоит». Глубокий проход идёт ~10 минут. */ export const STALL_MINUTES = 20; const REMIND_HOURS = 6; export interface WatchdogState { /** Когда отправлено последнее сообщение о простое; null — парсер в порядке. */ alertedAt: Date | null; } const ago = (from: Date, now: Date) => { const min = Math.round((now.getTime() - from.getTime()) / 60_000); return min < 120 ? `${min} мин` : `${Math.round(min / 60)} ч`; }; /** Что не так с парсером, или null, если всё в порядке. */ export async function parserProblem(db: Db, now = new Date()): Promise { const parser = await loadParserSettings(db); // В ночную паузу парсер спит по расписанию — это не простой. if (isQuietHours(parser, now)) return null; const stall = STALL_MINUTES * 60_000; if (!parser.enabled) { const [row] = await db.select({ at: settings.updatedAt }).from(settings).where(eq(settings.key, 'parser')); const since = row?.at ?? now; // Только что выключили руками — даём время включить обратно. return now.getTime() - since.getTime() >= stall ? `выключен в настройках (уже ${ago(since, now)})` : null; } const [last] = await db.select({ at: parserRuns.startedAt }).from(parserRuns).orderBy(desc(parserRuns.id)).limit(1); if (!last) return null; // Простой считается с конца ночной паузы: последний проход был вечером, а утром парсер только проснулся. const since = new Date(Math.max(last.at.getTime(), lastQuietEnd(parser, now)?.getTime() ?? 0)); return now.getTime() - since.getTime() >= stall ? `не запускался ${ago(last.at, now)}` : null; } /** Одна проверка: шлёт сообщение о простое (с напоминаниями) и о восстановлении. */ export async function runParserWatchdog(db: Db, state: WatchdogState, now = new Date()): Promise { const problem = await parserProblem(db, now); const remind = state.alertedAt && now.getTime() - state.alertedAt.getTime() >= REMIND_HOURS * 3_600_000; if (problem && (!state.alertedAt || remind)) { await notify(db, `⚠️ Парсер стоит: ${problem}. Новые объекты не приходят.\nВключить — Админка → Парсер.`); state.alertedAt = now; } else if (!problem && state.alertedAt) { await notify(db, '✅ Парсер снова работает.'); state.alertedAt = null; } } async function notify(db: Db, text: string) { const admins = await db .select({ id: users.id }) .from(users) .where(and(eq(users.role, 'superadmin'), isNotNull(users.telegramId), isNull(users.removedAt))); await enqueueRaw(db, admins.map((a) => a.id), text); }