diff --git a/backend/src/controllers/adminController.js b/backend/src/controllers/adminController.js index 9c6ce00..76a235f 100644 --- a/backend/src/controllers/adminController.js +++ b/backend/src/controllers/adminController.js @@ -648,6 +648,21 @@ function _dbTables() { } catch { return []; } } +// Активные проверки: отклик БД (мс) и тест записи на диск рядом с БД. +function _runChecks() { + let dbPingMs = null, dbOk = false; + try { const t = process.hrtime.bigint(); db.prepare('SELECT 1 AS ok').get(); dbPingMs = Number(process.hrtime.bigint() - t) / 1e6; dbOk = true; } catch {} + let diskWritable = false; + try { + const f = path.join(path.dirname(path.resolve(DB_PATH)), '.health-write-test'); + fs.writeFileSync(f, 'ok'); fs.unlinkSync(f); diskWritable = true; + } catch {} + return { dbPingMs, dbOk, diskWritable }; +} +const _recentErrStmt = db.prepare( + "SELECT id, level, message, route, method, created_at FROM error_log ORDER BY id DESC LIMIT 8" +); + function getHealth(_req, res) { const uptimeSec = process.uptime(); const mem = process.memoryUsage(); @@ -672,6 +687,10 @@ function getHealth(_req, res) { let sseStats = { users: 0, guests: 0, connections: 0 }; try { sseStats = sse.stats(); } catch {} + // Активные health-проверки (Level 4): отклик БД и запись на диск. + const checks = _runChecks(); + const recentErrorList = (() => { try { return _recentErrStmt.all(); } catch { return []; } })(); + // Вердикт здоровья по порогам. const reasons = []; let status = 'ok'; @@ -688,9 +707,13 @@ function getHealth(_req, res) { if (eventLoopLagMs > 200) crit(`Лаг event-loop ${eventLoopLagMs.toFixed(0)} мс`); else if (eventLoopLagMs > 70) warn(`Лаг event-loop ${eventLoopLagMs.toFixed(0)} мс`); if (dbSizeBytes > 1.5e9) warn('БД >1.5 ГБ'); + if (!checks.dbOk) crit('БД недоступна'); + if (!checks.diskWritable) crit('Диск недоступен для записи'); + if (checks.dbPingMs != null && checks.dbPingMs > 100) warn(`Медленный отклик БД ${checks.dbPingMs.toFixed(0)} мс`); res.json({ status, reasons, + checks, recentErrorList, uptime: uptimeSec, startedAt: new Date(Date.now() - uptimeSec * 1000).toISOString(), memory: { rss: mem.rss, heapUsed: mem.heapUsed, heapTotal: mem.heapTotal }, diff --git a/frontend/js/admin/admin.js b/frontend/js/admin/admin.js index b024712..830485c 100644 --- a/frontend/js/admin/admin.js +++ b/frontend/js/admin/admin.js @@ -388,6 +388,30 @@ `; } + // ── панель диагностики (Level 4): health-чеки + последние ошибки ── + let diagHtml = ''; + { + const ch = h.checks || {}; + const okCol = '#4ade80', badCol = 'var(--pink)'; + const chip = (label, ok, extra) => `
${label}${extra?` ${extra}`:''}
`; + const errs = h.recentErrorList || []; + const lvlCol = l => l==='error'||l==='fatal'?'var(--pink)':l==='warn'?'#facc15':'var(--text-3)'; + diagHtml = `
+
Диагностика
+
+ ${chip('База данных', !!ch.dbOk, ch.dbPingMs!=null?ch.dbPingMs.toFixed(2)+' мс':'')} + ${chip('Запись на диск', !!ch.diskWritable, ch.diskWritable?'доступна':'НЕДОСТУПНА')} +
+
Последние ошибки
+ ${errs.length ? errs.map(e=>`
+ ${esc((e.created_at||'').replace('T',' ').slice(5,16))} + ${esc(e.level||'')} + ${e.route?`${esc(e.method||'')} ${esc(e.route)}`:''} + ${esc(e.message||'')} +
`).join('') : `
Ошибок нет
`} +
`; + } + el.innerHTML = `
@@ -440,6 +464,8 @@ ${trendsHtml} + ${diagHtml} +
Крупнейшие таблицы БД
${(h.db.tables||[]).map(t=>`