Files
iistwin/server/workers/automation-scheduler.ts
Ильяс Султанов abd5d4b65f chore(ci+logs): eslint в CI, npm audit, logger с уровнями, retention error_logs
Шаги 1.6 и 1.7 плана production-готовности:
- eslint flat-config (баг-ловушки, легаси warn), lint блокирующий в pr-check
- npm audit --audit-level=high в CI (отчёт)
- фикс реального бага: условный useRef в TaskTitleInline
- server/utils/logger.ts (LOG_LEVEL/LOG_FORMAT) в 5 горячих местах
- error_logs retention 30 дней (worker), redactSensitive в captureErrorLog
- 0 errors lint, vitest 96/96
2026-09-08 00:11:38 +03:00

81 lines
3.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { storage } from "../storage";
import { withSuperAdmin } from "../db";
import { runAutomationInWorker } from "../routes/automation.routes";
import { logger } from "../utils/logger";
/**
* Планировщик schedule-автоматизаций.
*
* Раз в минуту проверяет активные автоматизации с trigger = 'schedule' всех
* организаций и запускает те, у которых время из triggerConfig.time (HH:MM, МСК)
* уже наступило сегодня, а запуска сегодня ещё не было. Отметка о запуске
* хранится в automations.last_scheduled_run_at, поэтому переживает рестарт
* контейнера; при даунтайме в момент HH:MM автоматизация запустится
* при первой возможности в тот же день (catch-up в пределах суток).
*/
const log = logger("Automation Scheduler");
const TICK_MS = 60_000;
const MSK_OFFSET_MS = 3 * 60 * 60 * 1000; // МСК = UTC+3, перехода на летнее время нет
function mskNow(): { today: string; minutesOfDay: number } {
const msk = new Date(Date.now() + MSK_OFFSET_MS);
return {
today: msk.toISOString().slice(0, 10),
minutesOfDay: msk.getUTCHours() * 60 + msk.getUTCMinutes(),
};
}
function parseTime(value: unknown): number | null {
if (typeof value !== "string") return null;
const m = /^(\d{1,2}):(\d{2})$/.exec(value.trim());
if (!m) return null;
const h = Number(m[1]);
const min = Number(m[2]);
if (h > 23 || min > 59) return null;
return h * 60 + min;
}
async function tick(): Promise<void> {
const { today, minutesOfDay } = mskNow();
const list = await withSuperAdmin(() => storage.getActiveScheduleAutomations());
for (const automation of list) {
try {
const config = (automation.triggerConfig as Record<string, unknown>) || {};
const target = parseTime(config.time);
if (target === null || target > minutesOfDay) continue;
if (automation.lastScheduledRunAt) {
const lastMskDay = new Date(automation.lastScheduledRunAt.getTime() + MSK_OFFSET_MS)
.toISOString()
.slice(0, 10);
if (lastMskDay >= today) continue; // сегодня уже запускалась
}
const { logs, error } = await withSuperAdmin(() =>
runAutomationInWorker(automation.code, automation.organizationId, {
trigger: "schedule",
scheduledAt: new Date().toISOString(),
})
);
await withSuperAdmin(() => storage.markAutomationScheduledRun(automation.id));
log.info(
`'${automation.name}' (id ${automation.id}, org ${automation.organizationId}): ${error ? "ERROR " + error : "ok"}`
);
for (const line of logs) log.info(` ${line}`);
} catch (err) {
log.error(`Failed automation ${automation.id}:`, err);
}
}
}
export function startAutomationScheduler(): void {
setInterval(() => {
tick().catch((err) => log.error("Tick error:", err));
}, TICK_MS);
log.info("started");
}