feat: P1 收尾 —— 事件面板区块与 90 天响应时间趋势

事件时间线(面板侧):
- 监控列表下方「最近事件」区块(最近 20 条:时间/类型/目标/详情),
  无事件自动隐藏;维护窗口内事件带「维护」标记;窄屏隐藏详情列

响应时间趋势(P1-2):
- 当日分桶在 {total,up} 外累计 latSum/latCount/latMax(仅在线样本)
- 快照输出 lat: { avg, max, days:[{key,avg}] }
- 卡片视图:可用率条下方新增「90 天日均延迟」细柱图(最近 30 天 + 均值)
- 表格视图:可用率列 tooltip 追加「90 天日均延迟 X ms(峰值 Y ms)」
- --demo 同步生成 90 天延迟样例(含起伏),静态部署也能看到效果

验证:verify-views 新增 [J] 段 5 条断言(趋势条/30 格/tooltip/事件空态/无溢出),
六套合计 159 项全通过(17+35+26+20+39+22)。

至此 P0(通知、证书)与 P1(事件时间线、维护窗口、响应时间趋势)全部落地。
This commit is contained in:
tamakyi
2026-10-14 00:32:59 +08:00
parent bdaa31f10c
commit 4bb5296525
14 changed files with 561 additions and 294 deletions
+51 -7
View File
@@ -1,4 +1,4 @@
const VERSION = '0.10.0';
const VERSION = '0.11.0';
const HB_LIMIT = 48;
const UPTIME_DAYS = 90;
const BROWSER_CONCURRENCY = 6;
@@ -226,9 +226,47 @@ function uptimeSummary(st) {
return null;
}
// 90 天日均延迟趋势(卡片视图):取最近 30 天的日均延迟画成细柱,右侧给 90 天均值
function latTrendBar(lat) {
const days = lat?.days?.slice(-30) ?? [];
if (days.length < 2) return '';
const max = Math.max(...days.map((d) => d.avg), 1);
const bars = days.map((d) => {
const h = Math.max(10, Math.round((d.avg / max) * 100));
return `<span class="lat-cell" style="height:${h}%" title="${escapeHtml(d.key)}:日均 ${d.avg} ms"></span>`;
}).join('');
return `<div class="lat-trend" title="90 天日均延迟 ${lat.avg} ms · 峰值 ${lat.max} ms(数据来自当日分桶累计)">
<span class="lat-label">延迟</span>
<span class="lat-bars">${bars}</span>
<span class="lat-avg">${lat.avg} ms</span>
</div>`;
}
// 最近事件区块(故障 / 恢复 / 证书阈值 / 维护窗口内的事件)
function renderEvents() {
const box = $('events');
if (!box) return;
const list = $('events-list');
const empty = $('events-empty');
const events = (state.status?.events ?? []).slice(0, 20);
box.hidden = events.length === 0;
empty.hidden = events.length > 0;
const count = $('events-count');
if (count) count.textContent = events.length ? `最近 ${events.length} 条` : '';
const label = { down: '故障', up: '恢复', tls: '证书', maintenance: '维护' };
list.innerHTML = events.map((e) => {
const maint = e.maintenance ? '<span class="ev-maint" title="维护窗口内,未计入可用率、未发送告警">维护</span>' : '';
return `<li class="ev ev-${escapeHtml(e.type)}">
<span class="ev-time" title="${escapeHtml(fmtTime(e.t))}">${escapeHtml(fmtRelative(e.t))}</span>
<span class="ev-type">${escapeHtml(label[e.type] ?? e.type)}${maint}</span>
<span class="ev-name" title="${escapeHtml(e.name)}">${escapeHtml(e.name)}</span>
<span class="ev-detail" title="${escapeHtml(e.detail ?? '')}">${escapeHtml(e.detail ?? '')}</span>
</li>`;
}).join('');
}
function uptimeBar(st) {
const daily = st?.daily;
const cells = lastNDays(UPTIME_DAYS).map((key) => {
const daily = st?.daily; const cells = lastNDays(UPTIME_DAYS).map((key) => {
const b = daily?.[key];
let cls = 'lv-none';
let tip = `${key} · 无数据`;
@@ -309,14 +347,14 @@ function renderCard(m) {
hb.innerHTML = heartbeatSvg(st?.history);
const up = document.createElement('div');
up.innerHTML = uptimeBar(st);
up.innerHTML = `${uptimeBar(st)}${latTrendBar(st?.lat)}`;
const detail = document.createElement('div');
detail.className = 'card-detail';
detail.title = st?.detail ?? '';
detail.textContent = st?.detail ?? '等待检测';
card.append(head, target, metrics, hb, up.firstElementChild ?? document.createElement('div'), detail);
card.append(head, target, metrics, hb, up, detail);
return card;
}
@@ -493,8 +531,13 @@ function tableRow(m, hide) {
`<td class="t-num">${st?.latency != null ? `${st.latency} ms` : '–'}</td>` +
`<td class="t-num">${escapeHtml(resultText(st, m))}</td>` +
`<td class="t-num" title="${escapeHtml(fmtTime(st?.lastCheck))}">${fmtRelative(st?.lastCheck)}</td>` +
`<td class="t-hb">${heartbeatSvg(st?.history)}</td>` +
`<td class="t-uptime">${up ? `<span class="u-mini">${miniUptimeBar(st?.daily)}</span><span class="t-pct">${up.pct.toFixed(1)}%</span>` : '<span class="t-pct">–</span>'}</td>`;
`<td class="t-hb">${heartbeatSvg(st?.history)}</td>`;
const latTip = st?.lat ? ` · 90 天日均延迟 ${st.lat.avg} ms(峰值 ${st.lat.max} ms)` : '';
const uptimeCell = document.createElement('td');
uptimeCell.className = 't-uptime';
uptimeCell.title = `${up ? `可用率 ${up.pct.toFixed(1)}%` : '可用率数据积累中'}${latTip}`;
uptimeCell.innerHTML = up ? `<span class="u-mini">${miniUptimeBar(st?.daily)}</span><span class="t-pct">${up.pct.toFixed(1)}%</span>` : '<span class="t-pct">–</span>';
tr.appendChild(uptimeCell);
return tr;
}
@@ -661,6 +704,7 @@ function renderAll() {
renderTabs();
renderViewSwitch();
renderView();
renderEvents();
}
function renderError(err) {