feat: чанкинг batch-create >100 задач + журнал ошибок API

B: BatchCreateDialog режет список на чанки ≤100 (лимит сервера), каждый
чанк со своим idempotency-key (requestId-N) — повтор после ошибки не
создаёт дублей; 10-минутный лок армируется только при полном успехе,
а не до запроса (раньше блокировал даже после 400).

D: таблица error_logs (миграция 0066), захват ответов >=400 в request
middleware (без 401/health/events, дедуп 60с), admin API GET/DELETE
/api/error-logs и вкладка «Журнал ошибок» в Настройках.
This commit is contained in:
2026-08-03 14:55:29 +03:00
parent 1bb6a53226
commit 1d5321ed0c
8 changed files with 406 additions and 21 deletions

View File

@@ -18,7 +18,7 @@ import { fileURLToPath } from "url";
import { isS3Enabled, ensureS3Bucket, streamFromS3 } from "./utils/s3";
import { getKnownEmbeddingDim, detectEmbeddingDimension, type LlmProviderConfig } from "./services/llm-provider";
import { decrypt as decryptSecret } from "./crypto";
import { fileUploads } from "@shared/schema";
import { fileUploads, errorLogs } from "@shared/schema";
import { authenticateToken, authenticateFileToken, type AuthenticatedRequest } from "./middleware/auth.middleware";
import { EXT_TO_MIME, getFileExt } from "./utils/upload";
import crypto from "crypto";
@@ -287,12 +287,55 @@ app.use((req, res, next) => {
}
log(logLine);
// Журнал ошибок API (просмотр админом в Настройках → «Журнал ошибок»)
captureErrorLog(req, res.statusCode, path, capturedJsonResponse);
}
});
next();
});
// --- Захват ошибок API в таблицу error_logs ---
// Шум исключаем: 401 (просроченные токены/SSE), health-check и SSE-поток.
// Дедупликация: одинаковые (method, path, status, message) не чаще раза в 60 сек.
const ERROR_LOG_EXCLUDED_PATHS = new Set(['/api/health', '/api/events']);
const errorLogDedupe = new Map<string, number>();
function captureErrorLog(req: Request, statusCode: number, path: string, body?: Record<string, any>) {
try {
if (statusCode < 400 || statusCode === 401) return;
if (ERROR_LOG_EXCLUDED_PATHS.has(path)) return;
const rawMessage = (body && (body.error || body.message)) || '';
const message = String(rawMessage).slice(0, 2000) || `HTTP ${statusCode}`;
const key = `${req.method} ${path} ${statusCode} ${message.slice(0, 200)}`;
const now = Date.now();
const lastSeen = errorLogDedupe.get(key) ?? 0;
if (now - lastSeen < 60_000) return;
errorLogDedupe.set(key, now);
// Периодическая чистка map, чтобы не разрастался
if (errorLogDedupe.size > 5000) {
for (const [k, ts] of errorLogDedupe) {
if (now - ts > 60_000) errorLogDedupe.delete(k);
}
}
const authReq = req as AuthenticatedRequest;
db.insert(errorLogs).values({
organizationId: authReq.organizationId ?? null,
userId: authReq.user?.id ?? null,
method: req.method,
path: path.slice(0, 500),
statusCode,
message,
}).catch((e: unknown) => console.error('[error-logs] insert failed:', e));
} catch (e) {
console.error('[error-logs] capture failed:', e);
}
}
/**
* Page seeds — canonical JS code stored in server/page-seeds/ as fallback/recovery.
* Seeds are applied ONLY when the current DB code contains known critical bugs.