feat(admin/health): System Health Level 4 — диагностика + последние ошибки

adminController.getHealth: активные health-проверки — отклик БД (ping, мс) и
тест записи на диск рядом с БД; вердикт уходит в critical при недоступной БД
или диске, warning при медленном отклике БД (>100мс). Плюс recentErrorList —
последние 8 записей error_log (level/route/method/message/время).

admin.js: панель «Диагностика» — индикаторы БД/диска (зелёный/красный) +
лента последних ошибок с цветом по уровню.

Проверено: checks {dbOk,dbPingMs,diskWritable}, список ошибок отдаётся.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-05-30 18:38:56 +03:00
parent 6a934ca6c6
commit a6567d0938
2 changed files with 49 additions and 0 deletions
@@ -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 },