diff --git a/README.md b/README.md index 9455828..102a498 100644 --- a/README.md +++ b/README.md @@ -436,6 +436,16 @@ UptimeRobot 的探测来自其多个节点,适合作为**异地视角**与自 每次状态翻转(故障 / 恢复 / 证书阈值)都会追加到 `data/events.json`(私有目录,保留最近 500 条),并通过 `GET /api/events` 读取。快照里也会带上最近 20 条供面板使用;`redactDetails` 开启时,快照内事件只保留类型与时间。 +**面板展示**:监控列表下方有一个「最近事件」区块(最多 20 条),每行显示 `时间 / 类型 / 目标 / 详情`;维护窗口内的事件会带「维护」标记(表示未计入可用率、未发告警)。无事件时区块自动隐藏。 + +### 响应时间趋势 + +当日分桶在 `{ total, up }` 之外还累计 `latSum / latCount / latMax`(仅在线样本),探针据此在快照里给出 `lat: { avg, max, days: [{ key, avg }] }`: + +- **卡片视图**:可用率条下方多一条「90 天日均延迟」细柱图(取最近 30 天),悬停看单日数值,右侧为 90 天均值 +- **表格视图**:可用率列的 tooltip 里追加 `90 天日均延迟 X ms(峰值 Y ms)` +- 数据从探针开始累计当天起累积,跑满 90 天后覆盖全周期;离线样本不计入延迟统计 + ## 部署方案 | 方案 | 架构 | 数据更新 | 成本 | 适合场景 | @@ -529,6 +539,15 @@ UptimeRobot 的探测来自其多个节点,适合作为**异地视角**与自 - [x] 页脚改为展示「数据更新于 … + 下次检查 Ns」,tooltip 内含服务端更新节奏与快照生成时间 - [x] `scripts/verify.cjs` 适配纯外部数据源部署(自动跳过不适用的探测类断言),并新增「倒计时对齐服务端」断言 +### v0.11.0 已实现(P1 收尾) + +- [x] **「最近事件」面板区块**:监控列表下方展示最近 20 条状态翻转(故障 / 恢复 / 证书 / 维护),无事件自动隐藏;窄屏隐藏「详情」列 +- [x] **90 天响应时间趋势**:当日分桶累计 `latSum/latCount/latMax`,快照输出 `lat: { avg, max, days[] }`;卡片视图在可用率条下新增延迟细柱图(最近 30 天 + 90 天均值),表格视图在可用率列 tooltip 追加日均/峰值 +- [x] `--demo` 同步生成 90 天延迟样例(含起伏形状),静态部署也能看到趋势效果 +- [x] `verify-views` 新增 [J] 段 5 条断言(趋势条渲染 / 30 格 / tooltip / 事件空态 / 无溢出),**P0+P1 全部完成** + +> 至此最初规划的 P0(通知、证书)与 P1(事件时间线、维护窗口、响应时间趋势)全部落地。 + ### v0.10.0 已实现(P0:通知 + 证书) - [x] **通知渠道①:邮件** —— 自写极简 SMTP 客户端(**零依赖**,不引 nodemailer):465 隐式 TLS / 587·25 自动 STARTTLS、AUTH LOGIN 与 PLAIN、UTF-8 主题按 RFC2047 编码 + base64 正文、多收件人、5xx 与连接错误可读;服务器不支持 STARTTLS 时**默认拒绝明文发信**(需显式 `allowInsecure`) diff --git a/docs/screenshot-demo.png b/docs/screenshot-demo.png index 8b833c8..aa08951 100644 Binary files a/docs/screenshot-demo.png and b/docs/screenshot-demo.png differ diff --git a/docs/screenshot-desktop.png b/docs/screenshot-desktop.png index 141f0a2..3a0e0f9 100644 Binary files a/docs/screenshot-desktop.png and b/docs/screenshot-desktop.png differ diff --git a/docs/screenshot-mobile.png b/docs/screenshot-mobile.png index ec85a87..65539a5 100644 Binary files a/docs/screenshot-mobile.png and b/docs/screenshot-mobile.png differ diff --git a/docs/screenshot-shiroko.png b/docs/screenshot-shiroko.png index caef2f2..d936a04 100644 Binary files a/docs/screenshot-shiroko.png and b/docs/screenshot-shiroko.png differ diff --git a/docs/screenshot-theme-default.png b/docs/screenshot-theme-default.png index afcc04c..5c60a15 100644 Binary files a/docs/screenshot-theme-default.png and b/docs/screenshot-theme-default.png differ diff --git a/docs/screenshot-view-cards.png b/docs/screenshot-view-cards.png index 324449a..e75c464 100644 Binary files a/docs/screenshot-view-cards.png and b/docs/screenshot-view-cards.png differ diff --git a/docs/screenshot-view-table.png b/docs/screenshot-view-table.png index b5b59d2..9c16f9e 100644 Binary files a/docs/screenshot-view-table.png and b/docs/screenshot-view-table.png differ diff --git a/edge/cloudflare/worker.js b/edge/cloudflare/worker.js index 06b47eb..414d520 100644 --- a/edge/cloudflare/worker.js +++ b/edge/cloudflare/worker.js @@ -11,7 +11,7 @@ import { connect } from 'cloudflare:sockets'; -const VERSION = '0.10.0'; +const VERSION = '0.11.0'; const HISTORY_LIMIT = 48; const UPTIME_DAYS = 90; const USER_AGENT = 'shiroko-monitor/0.3 (lightweight uptime panel)'; diff --git a/prober/node/index.js b/prober/node/index.js index 9ac000f..944f36c 100644 --- a/prober/node/index.js +++ b/prober/node/index.js @@ -37,7 +37,7 @@ const CONCURRENCY = 8; const WRITE_DEBOUNCE_MS = 300; const DEFAULT_INTERVAL_SEC = 60; const PORT = Number(process.env.PORT || 8787); -const VERSION = '0.10.0'; +const VERSION = '0.11.0'; const UR_DEFAULT_POLL_SEC = 300; const UR_MIN_REFRESH_GAP_MS = 60000; @@ -118,14 +118,43 @@ function pruneDaily(daily) { while (keys.length > UPTIME_DAYS) delete daily[keys.shift()]; } -function recordDaily(daily, ok) { +// 记录一次探测到「当日分桶」;latency 仅在在线时传入(离线结果没有有意义的延迟) +// 同时累计延迟统计(sum/count/max),用于面板的 90 天延迟趋势 +function recordDaily(daily, ok, latency = null) { const key = dayKey(); const bucket = daily[key] ?? (daily[key] = { total: 0, up: 0 }); bucket.total++; - if (ok) bucket.up++; + if (ok) { + bucket.up++; + if (Number.isFinite(latency) && latency >= 0) { + bucket.latSum = (bucket.latSum ?? 0) + latency; + bucket.latCount = (bucket.latCount ?? 0) + 1; + bucket.latMax = Math.max(bucket.latMax ?? 0, latency); + } + } pruneDaily(daily); } +// 从分桶聚合延迟趋势(90 天):返回 { avg, max, days: [{ key, avg }] } +function latencyTrend(daily) { + const keys = Object.keys(daily ?? {}).sort(); + const days = []; + let sum = 0; + let count = 0; + let max = 0; + for (const key of keys) { + const b = daily[key]; + if (!b?.latCount) continue; + const avg = Math.round(b.latSum / b.latCount); + days.push({ key, avg }); + sum += b.latSum; + count += b.latCount; + max = Math.max(max, b.latMax ?? 0); + } + if (!count) return null; + return { avg: Math.round(sum / count), max, days }; +} + function intervalMs(monitor) { const sec = Number(monitor.interval); // 非法或缺失的 interval 一律回落到默认值,避免算出 NaN 让该目标永久不再被调度 @@ -180,7 +209,7 @@ async function syncUptimeRobot(runtime, { minGapMs = 0 } = {}) { s.history = item.history; s.uptimeRatios = item.uptimeRatios; s.downSeconds = item.downSeconds; - if (item.status !== 'pending') recordDaily(s.daily, item.status === 'up'); + if (item.status !== 'pending') recordDaily(s.daily, item.status === 'up', item.latency); } // 清理 UR 侧已删除的监控 for (const [id, s] of runtime.state) { @@ -287,7 +316,7 @@ async function probeDue(runtime, force = false) { s.lastResult = result; s.history.push({ t: result.checkedAt, ok: result.status === 'up', ms: result.latency ?? 0 }); if (s.history.length > HISTORY_LIMIT) s.history.shift(); - recordDaily(s.daily, result.status === 'up'); + recordDaily(s.daily, result.status === 'up', result.latency); const tag = result.status === 'up' ? '[up] ' : '[down]'; console.log(`${tag} ${m.id.padEnd(20)} ${String(result.latency ?? '-').padStart(5)}ms ${result.detail}`); } catch (err) { @@ -633,6 +662,9 @@ function snapshot(runtime, demo = false) { const maintenance = activeMaintenanceFor(config, id); if (maintenance) entry.maintenance = { note: maintenance.note ?? null, to: maintenance.to ?? null }; } + // 90 天延迟趋势(自当日分桶聚合,自建与 UR 镜像目标都适用) + const trend = latencyTrend(s.daily); + if (trend) entry.lat = trend; monitors[id] = entry; } @@ -762,7 +794,10 @@ function writeDemo(config) { const key = dayKey(new Date(Date.now() - d * 86400000)); const missRatio = alwaysDown ? 1 : (seed + d * 13) % 23 === 0 ? 0.05 : (seed + d * 7) % 11 === 0 ? 0.012 : 0; const miss = Math.round(perDay * missRatio); - daily[key] = { total: perDay, up: perDay - miss }; + // 演示用延迟:随日期缓慢起伏,让 90 天趋势有形状可看 + const up = perDay - miss; + const avg = alwaysDown ? 0 : 40 + ((seed + d * 5) % 160); + daily[key] = { total: perDay, up, latSum: avg * up, latCount: up, latMax: avg + 60 }; } s.daily = daily; } diff --git a/public/assets/app.js b/public/assets/app.js index 10ce17d..15d6858 100644 --- a/public/assets/app.js +++ b/public/assets/app.js @@ -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 ``; + }).join(''); + return `
+ 延迟 + ${bars} + ${lat.avg} ms +
`; +} + +// 最近事件区块(故障 / 恢复 / 证书阈值 / 维护窗口内的事件) +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 ? '维护' : ''; + return `
  • + ${escapeHtml(fmtRelative(e.t))} + ${escapeHtml(label[e.type] ?? e.type)}${maint} + ${escapeHtml(e.name)} + ${escapeHtml(e.detail ?? '')} +
  • `; + }).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) { `${st?.latency != null ? `${st.latency} ms` : '–'}` + `${escapeHtml(resultText(st, m))}` + `${fmtRelative(st?.lastCheck)}` + - `${heartbeatSvg(st?.history)}` + - `${up ? `${miniUptimeBar(st?.daily)}${up.pct.toFixed(1)}%` : '–'}`; + `${heartbeatSvg(st?.history)}`; + 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 ? `${miniUptimeBar(st?.daily)}${up.pct.toFixed(1)}%` : '–'; + tr.appendChild(uptimeCell); return tr; } @@ -661,6 +704,7 @@ function renderAll() { renderTabs(); renderViewSwitch(); renderView(); + renderEvents(); } function renderError(err) { diff --git a/public/index.html b/public/index.html index 6b20ccf..b180633 100644 --- a/public/index.html +++ b/public/index.html @@ -50,6 +50,16 @@
    正在加载监控配置…
    + + + diff --git a/public/status.json b/public/status.json index ebcc92b..22b6b20 100644 --- a/public/status.json +++ b/public/status.json @@ -1,8 +1,8 @@ { "prober": "shiroko-monitor-node/0.10.0", "demo": false, - "generatedAt": "2026-10-13T15:54:42.793Z", - "dataUpdatedAt": "2026-10-13T15:53:59.134Z", + "generatedAt": "2026-10-13T16:32:57.040Z", + "dataUpdatedAt": "2026-10-13T16:32:52.050Z", "updateIntervalSec": 300, "dataFilter": "uptimerobot", "stats": { @@ -10,12 +10,12 @@ "up": 5, "down": 1, "pending": 0, - "avgLatency": 763, + "avgLatency": 764, "avgUptime90": 83.3, "memory": { - "rssMB": 67.4, + "rssMB": 66.8, "heapUsedMB": 12.7, - "heapTotalMB": 14.2 + "heapTotalMB": 16 } }, "sources": { @@ -23,10 +23,10 @@ "enabled": true, "ok": true, "error": null, - "lastSyncAt": "2026-10-13T15:51:29.877Z", + "lastSyncAt": "2026-10-13T16:30:34.184Z", "count": 6, "syncing": false, - "lastSyncMs": 1791906687760 + "lastSyncMs": 1791909032527 } }, "notify": { @@ -35,13 +35,61 @@ "quietHours": null, "failureThreshold": 2, "lastResult": { - "at": "2026-10-13T15:53:51.149Z", + "at": "2026-10-13T16:32:53.722Z", "suppressed": false, "skipped": "notify-disabled", "results": [] } }, "events": [ + { + "t": "2026-10-13T16:32:53.719Z", + "id": "example-com", + "name": "Example 站点", + "type": "up", + "detail": "已恢复", + "maintenance": false + }, + { + "t": "2026-10-13T16:31:53.271Z", + "id": "example-com", + "name": "Example 站点", + "type": "down", + "detail": "ETIMEDOUT", + "maintenance": false + }, + { + "t": "2026-10-13T16:26:07.133Z", + "id": "cloudflare-site", + "name": "Cloudflare 官网", + "type": "up", + "detail": "已恢复", + "maintenance": false + }, + { + "t": "2026-10-13T16:25:08.605Z", + "id": "cloudflare-site", + "name": "Cloudflare 官网", + "type": "down", + "detail": "UND_ERR_HEADERS_OVERFLOW", + "maintenance": false + }, + { + "t": "2026-10-13T16:13:41.867Z", + "id": "cloudflare-site", + "name": "Cloudflare 官网", + "type": "up", + "detail": "已恢复", + "maintenance": false + }, + { + "t": "2026-10-13T16:12:42.371Z", + "id": "cloudflare-site", + "name": "Cloudflare 官网", + "type": "down", + "detail": "ETIMEDOUT", + "maintenance": false + }, { "t": "2026-10-13T15:53:51.142Z", "id": "cloudflare-site", @@ -110,191 +158,194 @@ "monitors": { "ur-792059439": { "status": "up", - "latency": 233, + "latency": 396, "code": null, "detail": "UptimeRobot:运行正常", - "lastCheck": "2026-10-13T15:50:00.000Z", + "lastCheck": "2026-10-13T16:26:00.000Z", "history": [ { - "t": "2026-10-12T17:05:00.000Z", + "t": "2026-10-12T17:40:00.000Z", "ok": true, - "ms": 273 + "ms": 304 }, { - "t": "2026-10-12T17:45:00.000Z", + "t": "2026-10-12T18:20:00.000Z", "ok": true, - "ms": 391 + "ms": 323 }, { - "t": "2026-10-12T18:25:00.000Z", + "t": "2026-10-12T19:00:00.000Z", "ok": true, - "ms": 326 + "ms": 328 }, { - "t": "2026-10-12T19:05:00.000Z", + "t": "2026-10-12T19:40:00.000Z", "ok": true, - "ms": 333 + "ms": 304 }, { - "t": "2026-10-12T19:45:00.000Z", + "t": "2026-10-12T20:20:00.000Z", "ok": true, - "ms": 249 + "ms": 265 }, { - "t": "2026-10-12T20:25:00.000Z", - "ok": true, - "ms": 262 - }, - { - "t": "2026-10-12T21:05:00.000Z", - "ok": true, - "ms": 290 - }, - { - "t": "2026-10-12T21:45:00.000Z", - "ok": true, - "ms": 350 - }, - { - "t": "2026-10-12T22:25:00.000Z", - "ok": true, - "ms": 166 - }, - { - "t": "2026-10-12T23:10:00.000Z", - "ok": true, - "ms": 283 - }, - { - "t": "2026-10-12T23:50:00.000Z", - "ok": true, - "ms": 383 - }, - { - "t": "2026-10-13T00:30:00.000Z", - "ok": true, - "ms": 291 - }, - { - "t": "2026-10-13T01:10:00.000Z", - "ok": true, - "ms": 195 - }, - { - "t": "2026-10-13T01:50:00.000Z", - "ok": true, - "ms": 159 - }, - { - "t": "2026-10-13T02:30:00.000Z", - "ok": true, - "ms": 232 - }, - { - "t": "2026-10-13T03:10:00.000Z", - "ok": true, - "ms": 198 - }, - { - "t": "2026-10-13T03:50:00.000Z", - "ok": true, - "ms": 104 - }, - { - "t": "2026-10-13T04:35:00.000Z", - "ok": true, - "ms": 478 - }, - { - "t": "2026-10-13T05:15:00.000Z", - "ok": true, - "ms": 353 - }, - { - "t": "2026-10-13T05:55:00.000Z", + "t": "2026-10-12T21:00:00.000Z", "ok": true, "ms": 294 }, { - "t": "2026-10-13T06:35:00.000Z", + "t": "2026-10-12T21:40:00.000Z", "ok": true, - "ms": 238 + "ms": 379 }, { - "t": "2026-10-13T07:15:00.000Z", + "t": "2026-10-12T22:20:00.000Z", "ok": true, - "ms": 259 + "ms": 176 }, { - "t": "2026-10-13T07:55:00.000Z", + "t": "2026-10-12T23:00:00.000Z", "ok": true, - "ms": 227 + "ms": 325 }, { - "t": "2026-10-13T08:35:00.000Z", + "t": "2026-10-12T23:40:00.000Z", "ok": true, - "ms": 434 + "ms": 192 }, { - "t": "2026-10-13T09:15:00.000Z", + "t": "2026-10-13T00:20:00.000Z", "ok": true, - "ms": 333 + "ms": 342 }, { - "t": "2026-10-13T09:55:00.000Z", + "t": "2026-10-13T01:00:00.000Z", "ok": true, - "ms": 427 + "ms": 252 }, { - "t": "2026-10-13T10:35:00.000Z", + "t": "2026-10-13T01:40:00.000Z", "ok": true, - "ms": 233 + "ms": 164 }, { - "t": "2026-10-13T11:15:00.000Z", + "t": "2026-10-13T02:20:00.000Z", "ok": true, - "ms": 388 + "ms": 199 }, { - "t": "2026-10-13T11:55:00.000Z", + "t": "2026-10-13T03:00:00.000Z", "ok": true, - "ms": 329 + "ms": 216 }, { - "t": "2026-10-13T12:35:00.000Z", + "t": "2026-10-13T03:40:00.000Z", "ok": true, - "ms": 422 + "ms": 160 }, { - "t": "2026-10-13T13:15:00.000Z", + "t": "2026-10-13T04:20:00.000Z", + "ok": true, + "ms": 435 + }, + { + "t": "2026-10-13T05:00:00.000Z", + "ok": true, + "ms": 281 + }, + { + "t": "2026-10-13T05:40:00.000Z", + "ok": true, + "ms": 285 + }, + { + "t": "2026-10-13T06:20:00.000Z", + "ok": true, + "ms": 270 + }, + { + "t": "2026-10-13T07:00:00.000Z", + "ok": true, + "ms": 235 + }, + { + "t": "2026-10-13T07:40:00.000Z", "ok": true, "ms": 271 }, { - "t": "2026-10-13T13:55:00.000Z", + "t": "2026-10-13T08:20:00.000Z", "ok": true, - "ms": 307 + "ms": 417 }, { - "t": "2026-10-13T14:34:00.000Z", + "t": "2026-10-13T09:00:00.000Z", "ok": true, - "ms": 463 + "ms": 396 + }, + { + "t": "2026-10-13T09:40:00.000Z", + "ok": true, + "ms": 296 + }, + { + "t": "2026-10-13T10:20:00.000Z", + "ok": true, + "ms": 183 + }, + { + "t": "2026-10-13T11:00:00.000Z", + "ok": true, + "ms": 413 + }, + { + "t": "2026-10-13T11:40:00.000Z", + "ok": true, + "ms": 232 + }, + { + "t": "2026-10-13T12:20:00.000Z", + "ok": true, + "ms": 295 + }, + { + "t": "2026-10-13T13:00:00.000Z", + "ok": true, + "ms": 258 + }, + { + "t": "2026-10-13T13:40:00.000Z", + "ok": true, + "ms": 277 + }, + { + "t": "2026-10-13T14:20:00.000Z", + "ok": true, + "ms": 563 }, { "t": "2026-10-13T15:10:00.000Z", "ok": true, - "ms": 260 + "ms": 108 }, { "t": "2026-10-13T15:50:00.000Z", "ok": true, "ms": 233 + }, + { + "t": "2026-10-13T16:26:00.000Z", + "ok": true, + "ms": 396 } ], "daily": { - "2026-10-13": { + "2026-10-14": { "total": 6, - "up": 6 + "up": 6, + "latSum": 3003, + "latCount": 6, + "latMax": 538 } }, "meta": { @@ -317,195 +368,203 @@ "7": 0, "30": 0, "90": 0 + }, + "lat": { + "avg": 501, + "max": 538, + "days": [ + { + "key": "2026-10-14", + "avg": 501 + } + ] } }, "ur-795497022": { "status": "up", - "latency": 772, + "latency": 765, "code": null, "detail": "UptimeRobot:运行正常", - "lastCheck": "2026-10-13T15:48:00.000Z", + "lastCheck": "2026-10-13T16:25:00.000Z", "history": [ { - "t": "2026-10-12T16:35:00.000Z", + "t": "2026-10-12T17:35:00.000Z", "ok": true, - "ms": 700 + "ms": 714 }, { - "t": "2026-10-12T17:15:00.000Z", + "t": "2026-10-12T18:15:00.000Z", "ok": true, - "ms": 689 + "ms": 712 }, { - "t": "2026-10-12T17:55:00.000Z", + "t": "2026-10-12T18:55:00.000Z", "ok": true, - "ms": 725 + "ms": 728 }, { - "t": "2026-10-12T18:35:00.000Z", + "t": "2026-10-12T19:35:00.000Z", "ok": true, - "ms": 744 + "ms": 739 }, { - "t": "2026-10-12T19:15:00.000Z", + "t": "2026-10-12T20:15:00.000Z", "ok": true, - "ms": 723 + "ms": 751 }, { - "t": "2026-10-12T19:55:00.000Z", + "t": "2026-10-12T20:55:00.000Z", "ok": true, - "ms": 698 + "ms": 665 }, { - "t": "2026-10-12T20:35:00.000Z", + "t": "2026-10-12T21:35:00.000Z", "ok": true, - "ms": 768 + "ms": 632 }, { - "t": "2026-10-12T21:15:00.000Z", + "t": "2026-10-12T22:15:00.000Z", "ok": true, - "ms": 616 + "ms": 754 }, { - "t": "2026-10-12T21:55:00.000Z", + "t": "2026-10-12T22:55:00.000Z", "ok": true, - "ms": 626 + "ms": 628 }, { - "t": "2026-10-12T22:35:00.000Z", + "t": "2026-10-12T23:35:00.000Z", "ok": true, - "ms": 674 + "ms": 706 }, { - "t": "2026-10-12T23:15:00.000Z", - "ok": true, - "ms": 624 - }, - { - "t": "2026-10-12T23:55:00.000Z", + "t": "2026-10-13T00:15:00.000Z", "ok": true, "ms": 640 }, { - "t": "2026-10-13T00:35:00.000Z", + "t": "2026-10-13T00:55:00.000Z", "ok": true, - "ms": 673 + "ms": 732 }, { - "t": "2026-10-13T01:15:00.000Z", + "t": "2026-10-13T01:35:00.000Z", "ok": true, - "ms": 609 + "ms": 602 }, { - "t": "2026-10-13T01:55:00.000Z", + "t": "2026-10-13T02:15:00.000Z", "ok": true, - "ms": 759 + "ms": 656 }, { - "t": "2026-10-13T02:35:00.000Z", + "t": "2026-10-13T02:55:00.000Z", "ok": true, - "ms": 581 + "ms": 653 }, { - "t": "2026-10-13T03:15:00.000Z", + "t": "2026-10-13T03:35:00.000Z", "ok": true, - "ms": 666 + "ms": 726 }, { - "t": "2026-10-13T03:55:00.000Z", + "t": "2026-10-13T04:15:00.000Z", "ok": true, - "ms": 662 + "ms": 690 }, { - "t": "2026-10-13T04:35:00.000Z", + "t": "2026-10-13T04:55:00.000Z", "ok": true, - "ms": 685 + "ms": 653 }, { - "t": "2026-10-13T05:15:00.000Z", + "t": "2026-10-13T05:35:00.000Z", "ok": true, - "ms": 647 + "ms": 784 }, { - "t": "2026-10-13T05:55:00.000Z", + "t": "2026-10-13T06:15:00.000Z", "ok": true, - "ms": 588 + "ms": 679 }, { - "t": "2026-10-13T06:40:00.000Z", + "t": "2026-10-13T06:55:00.000Z", "ok": true, - "ms": 625 + "ms": 640 }, { - "t": "2026-10-13T07:20:00.000Z", + "t": "2026-10-13T07:35:00.000Z", "ok": true, - "ms": 673 + "ms": 696 }, { - "t": "2026-10-13T08:00:00.000Z", + "t": "2026-10-13T08:15:00.000Z", "ok": true, - "ms": 758 + "ms": 718 }, { - "t": "2026-10-13T08:40:00.000Z", + "t": "2026-10-13T08:55:00.000Z", "ok": true, - "ms": 826 + "ms": 939 }, { - "t": "2026-10-13T09:20:00.000Z", + "t": "2026-10-13T09:35:00.000Z", "ok": true, - "ms": 749 + "ms": 663 }, { - "t": "2026-10-13T10:00:00.000Z", + "t": "2026-10-13T10:15:00.000Z", "ok": true, - "ms": 791 + "ms": 837 }, { - "t": "2026-10-13T10:40:00.000Z", + "t": "2026-10-13T10:55:00.000Z", "ok": true, - "ms": 678 + "ms": 723 }, { - "t": "2026-10-13T11:20:00.000Z", + "t": "2026-10-13T11:35:00.000Z", "ok": true, - "ms": 740 + "ms": 688 }, { - "t": "2026-10-13T12:00:00.000Z", + "t": "2026-10-13T12:15:00.000Z", "ok": true, - "ms": 828 + "ms": 824 }, { - "t": "2026-10-13T12:40:00.000Z", + "t": "2026-10-13T12:55:00.000Z", "ok": true, - "ms": 722 + "ms": 799 }, { - "t": "2026-10-13T13:20:00.000Z", + "t": "2026-10-13T13:35:00.000Z", "ok": true, - "ms": 849 + "ms": 943 }, { - "t": "2026-10-13T14:43:00.000Z", + "t": "2026-10-13T14:20:00.000Z", "ok": true, - "ms": 845 - }, - { - "t": "2026-10-13T15:15:00.000Z", - "ok": true, - "ms": 857 + "ms": 808 }, { "t": "2026-10-13T15:48:00.000Z", "ok": true, - "ms": 772 + "ms": 782 + }, + { + "t": "2026-10-13T16:25:00.000Z", + "ok": true, + "ms": 765 } ], "daily": { - "2026-10-13": { + "2026-10-14": { "total": 6, - "up": 6 + "up": 6, + "latSum": 4408, + "latCount": 6, + "latMax": 868 } }, "meta": { @@ -519,29 +578,34 @@ }, "uptimeRatios": { "1": 100, - "7": 99.789, + "7": 100, "30": 99.951, "90": 99.93 }, "downSeconds": { "1": 0, - "7": 1276, + "7": 0, "30": 1276, "90": 5432 + }, + "lat": { + "avg": 735, + "max": 868, + "days": [ + { + "key": "2026-10-14", + "avg": 735 + } + ] } }, "ur-795556828": { "status": "up", - "latency": 230, + "latency": 359, "code": null, "detail": "UptimeRobot:运行正常", - "lastCheck": "2026-10-13T15:50:00.000Z", + "lastCheck": "2026-10-13T16:26:00.000Z", "history": [ - { - "t": "2026-10-12T16:55:00.000Z", - "ok": true, - "ms": 281 - }, { "t": "2026-10-12T17:40:00.000Z", "ok": true, @@ -623,100 +687,103 @@ "ms": 229 }, { - "t": "2026-10-13T04:30:00.000Z", + "t": "2026-10-13T04:25:00.000Z", "ok": true, - "ms": 234 + "ms": 255 }, { - "t": "2026-10-13T05:10:00.000Z", + "t": "2026-10-13T05:05:00.000Z", "ok": true, - "ms": 181 + "ms": 150 }, { - "t": "2026-10-13T05:50:00.000Z", + "t": "2026-10-13T05:45:00.000Z", "ok": true, - "ms": 206 + "ms": 201 }, { - "t": "2026-10-13T06:30:00.000Z", + "t": "2026-10-13T06:25:00.000Z", "ok": true, - "ms": 241 + "ms": 232 }, { - "t": "2026-10-13T07:10:00.000Z", + "t": "2026-10-13T07:05:00.000Z", "ok": true, - "ms": 241 + "ms": 247 }, { - "t": "2026-10-13T07:50:00.000Z", + "t": "2026-10-13T07:45:00.000Z", "ok": true, - "ms": 512 + "ms": 559 }, { - "t": "2026-10-13T08:30:00.000Z", + "t": "2026-10-13T08:25:00.000Z", "ok": true, - "ms": 216 + "ms": 219 }, { - "t": "2026-10-13T09:10:00.000Z", + "t": "2026-10-13T09:05:00.000Z", "ok": true, - "ms": 256 + "ms": 230 }, { - "t": "2026-10-13T09:55:00.000Z", + "t": "2026-10-13T09:45:00.000Z", "ok": true, - "ms": 455 + "ms": 432 }, { - "t": "2026-10-13T10:35:00.000Z", + "t": "2026-10-13T10:25:00.000Z", "ok": true, - "ms": 372 + "ms": 341 }, { - "t": "2026-10-13T11:15:00.000Z", + "t": "2026-10-13T11:05:00.000Z", "ok": true, - "ms": 240 + "ms": 233 }, { - "t": "2026-10-13T11:55:00.000Z", + "t": "2026-10-13T11:45:00.000Z", "ok": true, - "ms": 261 + "ms": 396 }, { - "t": "2026-10-13T12:35:00.000Z", + "t": "2026-10-13T12:25:00.000Z", "ok": true, - "ms": 335 + "ms": 308 }, { - "t": "2026-10-13T13:15:00.000Z", + "t": "2026-10-13T13:05:00.000Z", "ok": true, - "ms": 413 + "ms": 344 }, { - "t": "2026-10-13T13:55:00.000Z", + "t": "2026-10-13T13:45:00.000Z", "ok": true, - "ms": 283 + "ms": 278 }, { - "t": "2026-10-13T14:34:00.000Z", + "t": "2026-10-13T14:25:00.000Z", "ok": true, - "ms": 333 - }, - { - "t": "2026-10-13T15:10:00.000Z", - "ok": true, - "ms": 510 + "ms": 437 }, { "t": "2026-10-13T15:50:00.000Z", "ok": true, - "ms": 230 + "ms": 245 + }, + { + "t": "2026-10-13T16:26:00.000Z", + "ok": true, + "ms": 359 } ], "daily": { - "2026-10-13": { + "2026-10-14": { "total": 6, - "up": 6 + "up": 6, + "latSum": 1935, + "latCount": 6, + "latMax": 359 } }, "meta": { @@ -739,20 +806,25 @@ "7": 0, "30": 0, "90": 0 + }, + "lat": { + "avg": 323, + "max": 359, + "days": [ + { + "key": "2026-10-14", + "avg": 323 + } + ] } }, "ur-792059414": { "status": "up", - "latency": 2238, + "latency": 2003, "code": null, "detail": "UptimeRobot:运行正常", - "lastCheck": "2026-10-13T15:48:00.000Z", + "lastCheck": "2026-10-13T16:28:00.000Z", "history": [ - { - "t": "2026-10-12T17:10:00.000Z", - "ok": true, - "ms": 2816 - }, { "t": "2026-10-12T17:55:00.000Z", "ok": true, @@ -889,30 +961,38 @@ "ms": 2938 }, { - "t": "2026-10-13T13:40:00.000Z", + "t": "2026-10-13T13:35:00.000Z", "ok": true, - "ms": 3146 + "ms": 3044 }, { - "t": "2026-10-13T14:25:00.000Z", + "t": "2026-10-13T14:15:00.000Z", "ok": true, - "ms": 2125 + "ms": 2092 }, { - "t": "2026-10-13T15:08:00.000Z", + "t": "2026-10-13T14:55:00.000Z", "ok": true, - "ms": 2892 + "ms": 2732 }, { "t": "2026-10-13T15:48:00.000Z", "ok": true, - "ms": 2238 + "ms": 2321 + }, + { + "t": "2026-10-13T16:28:00.000Z", + "ok": true, + "ms": 2003 } ], "daily": { - "2026-10-13": { + "2026-10-14": { "total": 6, - "up": 6 + "up": 6, + "latSum": 12460, + "latCount": 6, + "latMax": 2270 } }, "meta": { @@ -926,29 +1006,34 @@ }, "uptimeRatios": { "1": 100, - "7": 99.787, + "7": 100, "30": 99.927, "90": 99.902 }, "downSeconds": { "1": 0, - "7": 1288, + "7": 0, "30": 1899, "90": 7593 + }, + "lat": { + "avg": 2077, + "max": 2270, + "days": [ + { + "key": "2026-10-14", + "avg": 2077 + } + ] } }, "ur-798788113": { "status": "up", - "latency": 340, + "latency": 297, "code": null, "detail": "UptimeRobot:运行正常", - "lastCheck": "2026-10-13T15:46:00.000Z", + "lastCheck": "2026-10-13T16:27:00.000Z", "history": [ - { - "t": "2026-10-12T16:50:00.000Z", - "ok": true, - "ms": 463 - }, { "t": "2026-10-12T17:30:00.000Z", "ok": true, @@ -1109,21 +1194,24 @@ "ok": true, "ms": 429 }, - { - "t": "2026-10-13T15:06:00.000Z", - "ok": true, - "ms": 443 - }, { "t": "2026-10-13T15:46:00.000Z", "ok": true, "ms": 340 + }, + { + "t": "2026-10-13T16:27:00.000Z", + "ok": true, + "ms": 297 } ], "daily": { - "2026-10-13": { + "2026-10-14": { "total": 6, - "up": 6 + "up": 6, + "latSum": 1914, + "latCount": 6, + "latMax": 352 } }, "meta": { @@ -1146,6 +1234,16 @@ "7": 0, "30": 0, "90": 0 + }, + "lat": { + "avg": 319, + "max": 352, + "days": [ + { + "key": "2026-10-14", + "avg": 319 + } + ] } }, "ur-796219015": { @@ -1162,7 +1260,7 @@ } ], "daily": { - "2026-10-13": { + "2026-10-14": { "total": 6, "up": 0 } diff --git a/scripts/verify-views.cjs b/scripts/verify-views.cjs index 1c19ab8..a9c522e 100644 --- a/scripts/verify-views.cjs +++ b/scripts/verify-views.cjs @@ -6,7 +6,10 @@ * 用法:NODE_PATH= node scripts/verify-views.cjs [baseUrl] */ const fs = require('node:fs'); +const http = require('node:http'); +const os = require('node:os'); const path = require('node:path'); +const { spawn } = require('node:child_process'); const { chromium } = require('playwright'); const BASE = process.argv[2] || 'http://localhost:8787'; @@ -244,6 +247,64 @@ async function main() { check(`${c.label}对比度 ≥${need}`, c.r >= need, `${c.r}:1`); } + // ---------- J. 事件区块与延迟趋势(演示快照,进程内临时服务) ---------- + console.log('\n[J] 事件区块与延迟趋势'); + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'shiroko-events-')); + try { + fs.cpSync(path.join(__dirname, '..', 'public'), tmpDir, { recursive: true }); + const cfgPath = path.join(tmpDir, 'monitors.json'); + const demoCfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8')); + demoCfg.settings.dataFilter = 'config'; + fs.writeFileSync(cfgPath, `${JSON.stringify(demoCfg, null, 2)}\n`); + await new Promise((resolve) => { + const child = spawn(process.execPath, [path.join(__dirname, '..', 'prober', 'node', 'index.js'), + '--once', '--demo', `--config=${cfgPath}`, `--local-config=${path.join(tmpDir, 'none.json')}`, + `--out=${path.join(tmpDir, 'status.json')}`], { stdio: 'ignore' }); + child.on('exit', (code) => resolve(code)); + }); + const demoServer = http.createServer((req, res) => { + const rel = decodeURIComponent(req.url.split('?')[0]); + const file = path.join(tmpDir, rel === '/' ? 'index.html' : rel); + fs.readFile(file, (err, data) => { + if (err) { + res.writeHead(404); + res.end(); + return; + } + const mime = { '.html': 'text/html;charset=utf-8', '.js': 'text/javascript', '.css': 'text/css', '.json': 'application/json', '.svg': 'image/svg+xml', '.webp': 'image/webp', '.png': 'image/png' }[path.extname(file)] ?? 'application/octet-stream'; + res.writeHead(200, { 'content-type': mime }); + res.end(data); + }); + }); + await new Promise((r) => demoServer.listen(8894, r)); + const dp = await browser.newPage({ viewport: { width: 1440, height: 900 } }); + await dp.goto('http://localhost:8894/?source=static&view=cards', { waitUntil: 'domcontentloaded' }); + await dp.waitForSelector('.card', { timeout: 20000 }); + await dp.waitForTimeout(1200); + const dj = await dp.evaluate(() => ({ + cards: document.querySelectorAll('.card').length, + trends: document.querySelectorAll('.lat-trend').length, + cellsPerTrend: document.querySelector('.lat-trend')?.children[1]?.children.length ?? 0, + trendTip: document.querySelector('.lat-trend')?.title ?? '', + eventsHidden: document.getElementById('events').hidden, + overflow: document.documentElement.scrollWidth - document.documentElement.clientWidth, + })); + check('卡片视图渲染 90 天延迟趋势条', dj.trends > 0 && dj.cellsPerTrend === 30, `${dj.trends} 条 × ${dj.cellsPerTrend} 格`); + check('趋势条 tooltip 含日均与峰值', /90 天日均延迟 \d+ ms · 峰值 \d+ ms/.test(dj.trendTip), dj.trendTip.slice(0, 60)); + check('无事件时区块隐藏', dj.eventsHidden === true, ''); + check('演示视图无横向溢出', dj.overflow <= 0, `${dj.overflow}px`); + // 表格视图:可用率列 tooltip 应带日均延迟 + await dp.evaluate(() => document.querySelector('.view-btn').click()); + await dp.waitForSelector('table.monitor-table', { timeout: 10000 }); + await dp.waitForTimeout(500); + const dt = await dp.evaluate(() => document.querySelector('tr[data-status] .t-uptime')?.title ?? ''); + check('表格可用率 tooltip 含 90 天日均延迟', /90 天日均延迟 \d+ ms/.test(dt), dt.slice(0, 60)); + await dp.close(); + await new Promise((r) => demoServer.close(r)); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + // 429 是刷新限流保护在起作用(快速连续导航各触发一次强制探测),面板已优雅降级为只读数据; // 这里只把 429 以外的错误视为失败 const hardErrors = errors.filter((e) => !/429/.test(e));