Шаги 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
81 lines
3.1 KiB
TypeScript
81 lines
3.1 KiB
TypeScript
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");
|
||
}
|