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));