feat: детали проверки в тултипе сегментов uptime
Build and deploy / build-deploy (push) Has been cancelled
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:
+86
-4
@@ -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, "'")}'></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 => ({
|
||||
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
|
||||
}[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];
|
||||
|
||||
Reference in New Issue
Block a user