From 6e1cd24b4e2067054d5133984fef111ff9cbcd05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9D=D0=B8=D0=BA=20=28Claude=29?= Date: Sat, 13 Jun 2026 10:23:24 +0300 Subject: [PATCH] feat: error logs API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit routes/admin.js: GET /logs — объединённые ошибки из 3 источников: generation_jobs (status=failed), ai_usage (!succeeded), scheduled_posts (status=failed) Сортировка по времени, топ-5 частых ошибок, группировка по типу --- src/routes/admin.js | 79 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/src/routes/admin.js b/src/routes/admin.js index 27467c3..7ad2223 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -326,3 +326,82 @@ router.delete('/queue/stuck', async (req, res) => { res.json({ ok: true, cleared: rows.length }); } catch (err) { res.status(500).json({ error: err.message }); } }); + +// ── ERROR LOGS ─────────────────────────────────────────────── + +// GET /api/admin/logs — последние ошибки из всех источников +router.get('/logs', async (req, res) => { + if (!await requireAdmin(req, res)) return; + const limit = Math.min(parseInt(req.query.limit || 50), 200); + try { + const [genFailed, aiErrors, scheduledFailed] = await Promise.all([ + // Ошибки генерации + query(` + SELECT + 'generation' as source, + j.id::text as entity_id, + j.type as operation, + j.error as message, + j.topic as context, + u.email as user_email, + j.created_at + FROM generation_jobs j + LEFT JOIN users u ON u.id = j.user_id + WHERE j.status = 'failed' AND j.error IS NOT NULL + ORDER BY j.created_at DESC LIMIT $1 + `, [limit]), + + // Ошибки AI провайдеров + query(` + SELECT + 'ai_provider' as source, + id::text as entity_id, + (provider || '/' || request_type) as operation, + error_message as message, + left(model, 60) as context, + NULL as user_email, + created_at + FROM ai_usage + WHERE NOT succeeded AND error_message IS NOT NULL + ORDER BY created_at DESC LIMIT $1 + `, [limit]), + + // Ошибки публикации постов + query(` + SELECT + 'publish' as source, + sp.id::text as entity_id, + (c.platform || ' publish') as operation, + 'Failed scheduled post' as message, + left(sp.custom_text, 60) as context, + NULL as user_email, + sp.scheduled_at as created_at + FROM scheduled_posts sp + JOIN channels c ON c.id = sp.channel_id + WHERE sp.status = 'failed' + ORDER BY sp.scheduled_at DESC LIMIT 20 + `), + ]); + + // Объединяем и сортируем + const all = [ + ...genFailed.rows, + ...aiErrors.rows, + ...scheduledFailed.rows, + ].sort((a, b) => new Date(b.created_at) - new Date(a.created_at)) + .slice(0, limit); + + // Группируем по типу ошибки для статистики + const errorGroups = {}; + for (const e of all) { + const key = e.message?.split('\n')[0]?.slice(0, 80) || 'unknown'; + errorGroups[key] = (errorGroups[key] || 0) + 1; + } + const topErrors = Object.entries(errorGroups) + .sort((a, b) => b[1] - a[1]) + .slice(0, 5) + .map(([msg, cnt]) => ({ msg, cnt })); + + res.json({ errors: all, total: all.length, topErrors }); + } catch (err) { res.status(500).json({ error: err.message }); } +});