feat: детали проверки в тултипе сегментов uptime
Build and deploy / build-deploy (push) Has been cancelled

Сегмент при наведении показывает тултип: успех/провал, время
проверки, латентность и текст ошибки.

Бэкенд: в таблицу checks добавлена колонка error (миграция через
ALTER TABLE), probe.py и ручной /api/check теперь сохраняют текст
ошибки, /api/models отдаёт детали каждой проверки в history.
This commit is contained in:
2026-10-21 22:24:17 +00:00
parent 3f6b5768d0
commit 1fb770fc52
3 changed files with 122 additions and 19 deletions
+86 -4
View File
@@ -146,6 +146,25 @@
.uptime-seg.empty { background: transparent; border: 1px dashed var(--border); box-sizing: border-box; }
.uptime-segs .uptime { margin-left: 6px; }
/* Тултип с деталями проверки */
.seg-tip {
position: fixed; z-index: 1000; pointer-events: none;
background: var(--bg2); border: 1px solid var(--border);
border-radius: 6px; padding: 8px 10px;
box-shadow: 0 6px 20px rgba(0,0,0,.45);
font-size: 12px; line-height: 1.45; color: var(--text);
max-width: 380px; white-space: pre-wrap; word-break: break-word;
display: none;
}
.seg-tip .tip-head { font-weight: 600; margin-bottom: 4px; }
.seg-tip .tip-ok { color: var(--green); }
.seg-tip .tip-fail { color: var(--red); }
.seg-tip .tip-meta { color: var(--text2); font-size: 11px; }
.seg-tip .tip-err {
color: var(--red); font-family: Consolas, 'Fira Mono', monospace;
font-size: 11px; margin-top: 4px; white-space: pre-wrap;
}
.check-btn {
background: none; border: 1px solid var(--border); border-radius: 4px;
color: var(--text2); padding: 3px 8px; cursor: pointer; font-size: 11px;
@@ -250,13 +269,15 @@ function uptimeCell(pct, history) {
: (pct >= 90 ? 'uptime-high' : pct >= 70 ? 'uptime-mid' : 'uptime-low');
// Сегменты: каждый — одна проверка. Слева старее, справа свежее.
const segs = hist.map(ok =>
`<span class="uptime-seg ${ok ? 'ok' : 'fail'}" title="${ok ? 'успех' : 'провал'}"></span>`
).join('');
const segs = hist.map((h, i) => {
const ok = typeof h === 'boolean' ? h : h && h.ok;
const data = typeof h === 'boolean' ? { ok } : h;
return `<span class="uptime-seg ${ok ? 'ok' : 'fail'}" data-seg='${JSON.stringify(data).replace(/'/g, "&#39;")}'></span>`;
}).join('');
const label = pct === null || pct === undefined ? '—' : `${pct}%`;
const tip = hist.length
? `title="последние ${hist.length} проверок: ${hist.filter(Boolean).length} успех"`
? `title="последние ${hist.length} проверок: ${hist.filter(h => typeof h === 'boolean' ? h : h && h.ok).length} успех"`
: '';
return `<div class="uptime-segs" ${tip}>
@@ -265,6 +286,67 @@ function uptimeCell(pct, history) {
</div>`;
}
// ─── Тултип с деталями проверки ──────────────────────────────────────────────
let segTipEl = null;
function segTip() {
if (!segTipEl) {
segTipEl = document.createElement('div');
segTipEl.className = 'seg-tip';
document.body.appendChild(segTipEl);
}
return segTipEl;
}
function fmtTime(iso) {
if (!iso) return '—';
const d = new Date(iso);
if (isNaN(d)) return iso;
return d.toLocaleString('ru', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' });
}
function showSegTip(el) {
let data;
try { data = JSON.parse(el.dataset.seg); } catch (e) { return; }
const tip = segTip();
const ok = data.ok;
const lat = data.latency_ms !== null && data.latency_ms !== undefined ? `${data.latency_ms} мс` : '—';
tip.innerHTML = `
<div class="tip-head ${ok ? 'tip-ok' : 'tip-fail'}">${ok ? '✓ Успех' : '✗ Провал'}</div>
<div class="tip-meta">${fmtTime(data.at)} · ${lat}</div>
${data.error ? `<div class="tip-err">${escapeHtml(data.error)}</div>` : ''}
`;
tip.style.display = 'block';
const r = el.getBoundingClientRect();
const tr = tip.getBoundingClientRect();
let left = r.left + r.width / 2 - tr.width / 2;
left = Math.max(8, Math.min(left, window.innerWidth - tr.width - 8));
let top = r.top - tr.height - 8;
if (top < 8) top = r.bottom + 8;
tip.style.left = `${left + window.scrollX}px`;
tip.style.top = `${top + window.scrollY}px`;
}
function hideSegTip() {
if (segTipEl) segTipEl.style.display = 'none';
}
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, c => ({
'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
}[c]));
}
// Делегирование: тултип на весь контейнер сегментов.
document.addEventListener('mouseover', e => {
const seg = e.target.closest('.uptime-seg[data-seg]');
if (seg) showSegTip(seg);
});
document.addEventListener('mouseout', e => {
if (e.target.closest('.uptime-seg[data-seg]')) hideSegTip();
});
document.addEventListener('scroll', hideSegTip, true);
function modelName(id) {
const parts = id.split('/');
const tail = parts[parts.length - 1];