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
+26
View File
@@ -388,6 +388,30 @@
</div></div>`;
}
// ── панель диагностики (Level 4): health-чеки + последние ошибки ──
let diagHtml = '';
{
const ch = h.checks || {};
const okCol = '#4ade80', badCol = 'var(--pink)';
const chip = (label, ok, extra) => `<div style="display:flex;align-items:center;gap:6px;font-size:.82rem"><span style="width:9px;height:9px;border-radius:50%;background:${ok?okCol:badCol}"></span>${label}${extra?` <span style="color:var(--text-3)">${extra}</span>`:''}</div>`;
const errs = h.recentErrorList || [];
const lvlCol = l => l==='error'||l==='fatal'?'var(--pink)':l==='warn'?'#facc15':'var(--text-3)';
diagHtml = `<div class="adm-panel" style="margin:14px 0 0">
<div class="adm-panel-title">Диагностика</div>
<div style="display:flex;gap:20px;flex-wrap:wrap;margin:6px 0 14px">
${chip('База данных', !!ch.dbOk, ch.dbPingMs!=null?ch.dbPingMs.toFixed(2)+' мс':'')}
${chip('Запись на диск', !!ch.diskWritable, ch.diskWritable?'доступна':'НЕДОСТУПНА')}
</div>
<div style="font-size:.72rem;color:var(--text-3);font-weight:700;text-transform:uppercase;margin-bottom:5px">Последние ошибки</div>
${errs.length ? errs.map(e=>`<div style="display:flex;align-items:baseline;gap:8px;font-size:.78rem;padding:3px 0;border-bottom:1px solid rgba(255,255,255,.04)">
<span style="color:var(--text-3);white-space:nowrap;font-size:.72rem">${esc((e.created_at||'').replace('T',' ').slice(5,16))}</span>
<span style="color:${lvlCol(e.level)};font-weight:700;white-space:nowrap">${esc(e.level||'')}</span>
${e.route?`<span style="color:var(--text-3);white-space:nowrap">${esc(e.method||'')} ${esc(e.route)}</span>`:''}
<span style="flex:1;color:var(--text-2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(e.message||'')}</span>
</div>`).join('') : `<div style="color:var(--green);font-size:.82rem">Ошибок нет</div>`}
</div>`;
}
el.innerHTML = `
<div class="adm-panel" style="margin:0 0 16px;padding:14px 18px;display:flex;align-items:center;gap:14px;border-left:4px solid ${stColor}">
<div style="width:13px;height:13px;border-radius:50%;background:${stColor};box-shadow:0 0 12px ${stColor};flex-shrink:0"></div>
@@ -440,6 +464,8 @@
${trendsHtml}
${diagHtml}
<div class="adm-panel" style="margin:14px 0 0">
<div class="adm-panel-title">Крупнейшие таблицы БД</div>
${(h.db.tables||[]).map(t=>`<div style="display:flex;align-items:center;gap:10px;margin:4px 0">