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
This commit is contained in:
2026-09-08 00:11:38 +03:00
parent 6eaca79cc6
commit abd5d4b65f
14 changed files with 1579 additions and 19 deletions

View File

@@ -1,6 +1,7 @@
import { storage } from "../storage";
import { withSuperAdmin } from "../db";
import { runAutomationInWorker } from "../routes/automation.routes";
import { logger } from "../utils/logger";
/**
* Планировщик schedule-автоматизаций.
@@ -13,6 +14,8 @@ import { runAutomationInWorker } from "../routes/automation.routes";
* при первой возможности в тот же день (catch-up в пределах суток).
*/
const log = logger("Automation Scheduler");
const TICK_MS = 60_000;
const MSK_OFFSET_MS = 3 * 60 * 60 * 1000; // МСК = UTC+3, перехода на летнее время нет
@@ -59,19 +62,19 @@ async function tick(): Promise<void> {
);
await withSuperAdmin(() => storage.markAutomationScheduledRun(automation.id));
console.log(
`[Automation Scheduler] '${automation.name}' (id ${automation.id}, org ${automation.organizationId}): ${error ? "ERROR " + error : "ok"}`
log.info(
`'${automation.name}' (id ${automation.id}, org ${automation.organizationId}): ${error ? "ERROR " + error : "ok"}`
);
for (const line of logs) console.log(`[Automation Scheduler] ${line}`);
for (const line of logs) log.info(` ${line}`);
} catch (err) {
console.error(`[Automation Scheduler] Failed automation ${automation.id}:`, err);
log.error(`Failed automation ${automation.id}:`, err);
}
}
}
export function startAutomationScheduler(): void {
setInterval(() => {
tick().catch((err) => console.error("[Automation Scheduler] Tick error:", err));
tick().catch((err) => log.error("Tick error:", err));
}, TICK_MS);
console.log("[Automation Scheduler] started");
log.info("started");
}

View File

@@ -0,0 +1,50 @@
import { db, withSuperAdmin } from "../db";
import { sql } from "drizzle-orm";
import { logger } from "../utils/logger";
/**
* Retention журнала ошибок API (таблица error_logs, см. shared/schema.ts).
*
* Раз в сутки удаляет записи старше RETENTION_DAYS дней. Без отдельной
* миграции: таблица и индексы уже существуют. Таймер unref'нут, чтобы
* не удерживать процесс (и не мешать тестам завершаться).
* Первый запуск — сразу при старте, дальше — каждые 24 часа.
*/
const log = logger("error-logs-retention");
const TICK_MS = 24 * 60 * 60 * 1000; // раз в сутки
const RETENTION_DAYS = 30;
let isRunning = false;
async function cleanup(): Promise<void> {
// withSuperAdmin — единая точка для фоновых задач вне контекста организации
const result = await withSuperAdmin(() =>
db.execute(sql`
DELETE FROM error_logs
WHERE created_at < now() - make_interval(days => ${RETENTION_DAYS})
`)
);
const deleted = (result as { rowCount?: number }).rowCount ?? 0;
if (deleted > 0) {
log.info(`Удалено устаревших записей error_logs: ${deleted} (старше ${RETENTION_DAYS} дней)`);
} else {
log.debug(`Устаревших записей error_logs нет (старше ${RETENTION_DAYS} дней)`);
}
}
export function startErrorLogsRetention(): void {
if (isRunning) return;
isRunning = true;
const timer = setInterval(() => {
cleanup().catch((err) => log.error("Cleanup error:", err));
}, TICK_MS);
timer.unref?.();
// Первый прогон сразу после старта (в фоне, не блокируя listen)
cleanup().catch((err) => log.error("Initial cleanup error:", err));
log.info(`started (retention ${RETENTION_DAYS} дней, тик раз в сутки)`);
}