diff --git a/README.md b/README.md index 102a498..7096e01 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,8 @@ shiroko-monitor/ "theme": "shiroko", // shiroko(砂狼白子)| default "artwork": "", // 立绘图片路径(相对 public/),留空则不显示 "decoOpacity": 0.78, // 人物/单车装饰的全局透明度(0~1,一个值同时作用于两者) - "dataFilter": "both", // both | config | uptimerobot(详见「面板形态与数据源」) + "dataFilter": "both", // 展示口径:both | config | uptimerobot(详见「面板形态与数据源」) + "probeScope": "all", // 探测范围:all | config | uptimerobot;不填则**跟随 dataFilter** "refreshInterval": 30, // 面板检查数据的最大间隔(秒,最小 5);实际对齐服务端更新节奏 "dataSource": "auto", // auto | api | static | browser "allowRefresh": true, // 是否允许触发强制探测(公网部署建议 false;详见 docs/SECURITY.md) @@ -539,6 +540,21 @@ UptimeRobot 的探测来自其多个节点,适合作为**异地视角**与自 - [x] 页脚改为展示「数据更新于 … + 下次检查 Ns」,tooltip 内含服务端更新节奏与快照生成时间 - [x] `scripts/verify.cjs` 适配纯外部数据源部署(自动跳过不适用的探测类断言),并新增「倒计时对齐服务端」断言 +### v0.11.1 已实现 + +**修复你反馈的三个问题** + +- [x] **表格显示不全**:`table-layout: fixed` 保证了不横向滚动,但列内仍是 `nowrap`,实测**长 URL 被截掉 388px、名称截掉 219px、可用率条截掉 35px**(1440 宽下共 14 个单元格被截断)。改为**允许换行**(地址 `break-all`、名称/结果 `overflow-wrap: anywhere`)并重排列宽比例——数据完整显示且**仍然零横向滚动**(已加断言:注入超长名称/地址后截断单元格 = 0) +- [x] **「最近事件」完全没样式**:原因是上次追加 CSS 的命令是 `node --check app.js && cat >> style.css`,`--check` 报错导致 `&&` 短路,**CSS 根本没写进文件**(只有 HTML 裸奔)。这次用编辑器写入并加了断言。同时**重做为时间线样式**:左侧竖线 + 按类型着色的圆点、类型徽标、左描边分色;避免短名称后面拖出大片空白(名称列按内容自适应);**默认折叠为最近 8 条**,可「展开全部 N 条」 +- [x] **`dataFilter: uptimerobot` 仍在探测本地目标**:新增 `settings.probeScope`(`all` / `config` / `uptimerobot`),**不填则跟随 `dataFilter`** —— 只展示一个来源时不再探测另一个来源的目标(省资源、少打扰);想保留探测可显式设 `"probeScope": "all"`。启动日志会明确打印当前口径与范围 + +**顺带修掉两个连带问题** + +- [x] **UR 镜像目标此前不产生事件也不告警**:`evaluateRound` 只遍历本地配置目标,而配了 `uptimerobot` 口径时你关心的恰恰是 UR 目标 —— 等于「收不到任何提醒」。现在两类目标统一纳入事件与告警 +- [x] **去抖计数被评估频率放大**:`evaluateRound` 每秒被调用,原来每次评估都让 `failStreak +1`,一个持续离线的目标 1 秒就凑够「连续 2 次失败」触发告警。改为**按探测时间戳去重**,只在真正产生新结果的那一轮累加;同时无状态变化时不再反复落盘 + +**测试**:`verify-views` [J] 段扩到 11 条(长内容不截断 / 仍不滚动 / 事件时间线布局 / 类型着色 / 折叠与展开),`verify-notify` 新增 [D3] 4 条(探测范围跟随、不再探测本地、显式 all 恢复探测、探测范围与展示口径解耦)与去抖计数断言。 + ### v0.11.0 已实现(P1 收尾) - [x] **「最近事件」面板区块**:监控列表下方展示最近 20 条状态翻转(故障 / 恢复 / 证书 / 维护),无事件自动隐藏;窄屏隐藏「详情」列 diff --git a/docs/screenshot-desktop.png b/docs/screenshot-desktop.png index 3a0e0f9..db1d140 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 65539a5..49e74b4 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 d936a04..b5407ef 100644 Binary files a/docs/screenshot-shiroko.png and b/docs/screenshot-shiroko.png differ diff --git a/docs/screenshot-view-cards.png b/docs/screenshot-view-cards.png index e75c464..1cfd878 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 9c16f9e..e87f202 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 414d520..1a00b69 100644 --- a/edge/cloudflare/worker.js +++ b/edge/cloudflare/worker.js @@ -11,7 +11,7 @@ import { connect } from 'cloudflare:sockets'; -const VERSION = '0.11.0'; +const VERSION = '0.11.1'; 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 944f36c..13090c0 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.11.0'; +const VERSION = '0.11.1'; const UR_DEFAULT_POLL_SEC = 300; const UR_MIN_REFRESH_GAP_MS = 60000; @@ -295,7 +295,7 @@ async function probeDue(runtime, force = false) { const { config, state } = runtime; const now = Date.now(); const due = []; - for (const m of config.monitors) { + for (const m of localProbeTargets(config)) { const s = state.get(m.id); if (!s || s.inFlight) continue; // UptimeRobot 镜像目标由其自身节点探测,本探针只拉取结果 @@ -408,7 +408,7 @@ const EVENTS_PATH = () => path.join(DATA_DIR, 'events.json'); function monitorNotifyState(runtime, id) { const list = runtime.notifyState.monitors; - if (!list[id]) list[id] = { lastStatus: null, failStreak: 0, notifiedTls: [], tlsValidTo: null }; + if (!list[id]) list[id] = { lastStatus: null, failStreak: 0, lastCheckedAt: null, notifiedTls: [], tlsValidTo: null }; return list[id]; } @@ -423,12 +423,28 @@ async function evaluateRound(runtime) { const notify = notifyConfig(runtime.config); const events = []; const pendingNotifications = []; + let changed = false; - for (const m of runtime.config.monitors) { + // 事件与告警覆盖两类目标:本地探测目标 + UptimeRobot 镜像目标 + // (UR 镜像目标同样会发生状态翻转,只拉数据不告警等于「配了 uptimerobot 口径就收不到任何提醒」) + const targets = localProbeTargets(runtime.config).map((m) => ({ id: m.id, name: m.name, target: m.url ?? `${m.host}:${m.port}` })); + for (const [id, s] of runtime.state) { + if (s.monitor?.source !== 'uptimerobot') continue; + targets.push({ id, name: s.monitor.name, target: s.monitor.url ?? '', source: 'uptimerobot' }); + } + + for (const m of targets) { const r = runtime.state.get(m.id)?.lastResult; if (!r || r.status === 'pending') continue; const st = monitorNotifyState(runtime, m.id); - st.failStreak = r.status === 'down' ? (st.failStreak ?? 0) + 1 : 0; + // 去抖计数只应该跟着「新的探测结果」走:本函数每秒被调用,若每次评估都累加, + // 一个持续离线的目标会在 1 秒内就凑够「连续失败 N 次」,阈值形同虚设 + const stamp = r.checkedAt ?? null; + if (st.lastCheckedAt !== stamp) { + st.lastCheckedAt = stamp; + st.failStreak = r.status === 'down' ? (st.failStreak ?? 0) + 1 : 0; + changed = true; + } const maintenance = activeMaintenanceFor(runtime.config, m.id); const at = new Date().toISOString(); @@ -436,27 +452,30 @@ async function evaluateRound(runtime) { // 去抖:连续失败达到阈值才算确认故障(阈值 1 表示立即告警) if (st.lastStatus === 'down' || st.failStreak < notify.failureThreshold) continue; st.lastStatus = 'down'; + changed = true; st.lastNotifiedAt = at; events.push({ t: at, id: m.id, name: m.name, type: 'down', detail: r.detail ?? null, maintenance: Boolean(maintenance) }); if (!maintenance) { pendingNotifications.push({ subject: `【故障】${m.name} 连接失败`, - text: `目标:${m.name}\n地址:${m.url ?? `${m.host}:${m.port}`}\n时间:${at}\n详情:${r.detail ?? '无'}\n连续失败:${st.failStreak} 次\n\n—— shiroko-monitor`, + text: `目标:${m.name}\n地址:${m.target}\n时间:${at}\n详情:${r.detail ?? '无'}\n连续失败:${st.failStreak} 次\n\n—— shiroko-monitor`, }); } } else if (r.status === 'up') { if (st.lastStatus === null) { st.lastStatus = 'up'; // 首次观测不产生「恢复」噪音 + changed = true; continue; } if (st.lastStatus === 'down') { st.lastStatus = 'up'; st.lastNotifiedAt = at; + changed = true; events.push({ t: at, id: m.id, name: m.name, type: 'up', detail: '已恢复', maintenance: Boolean(maintenance) }); if (notify.recoveryNotify && !maintenance) { pendingNotifications.push({ subject: `【恢复】${m.name} 已恢复正常`, - text: `目标:${m.name}\n地址:${m.url ?? `${m.host}:${m.port}`}\n恢复时间:${at}\n延迟:${r.latency ?? '—'} ms\n\n—— shiroko-monitor`, + text: `目标:${m.name}\n地址:${m.target}\n恢复时间:${at}\n延迟:${r.latency ?? '—'} ms\n\n—— shiroko-monitor`, }); } } @@ -477,9 +496,10 @@ async function evaluateRound(runtime) { }; const res = await notifyAll(notify, message, { logger: console }); runtime.notifyState.lastResult = { at: new Date().toISOString(), ...res }; + changed = true; } - // 无论是否发通知都要落盘:否则「连续失败计数」会丢,重启或下一轮就达不到阈值 - saveNotifyState(runtime); + // 只在状态真变化时落盘:tick 会每秒调用本函数,无变化时不写文件 + if (changed) saveNotifyState(runtime); } // 证书检查:按 checkIntervalSec 逐个握手,跨阈值时告警(同一张证书每个阈值只告一次) @@ -490,7 +510,7 @@ async function checkCertificates(runtime) { const events = []; const messages = []; - for (const m of runtime.config.monitors) { + for (const m of localProbeTargets(runtime.config)) { const target = tlsTargetOf(m); if (!target) continue; const prev = runtime.tls.get(m.id); @@ -554,6 +574,26 @@ function dataFilter(config) { return ['both', 'config', 'uptimerobot'].includes(f) ? f : 'both'; } +// 探测范围(是否真的去探本地配置里的目标): +// 显式设置 settings.probeScope 优先(all | config | uptimerobot) +// 未设置时**跟随 dataFilter** —— 面板只要一个来源的数据,就没必要再把另一个来源的目标探测一遍 +// (既省资源,也避免不必要的对外请求;想保留探测可显式设 "all") +function probeScopeSetting(config) { + const p = config.settings?.probeScope; + if (['all', 'config', 'uptimerobot'].includes(p)) return p; + const f = dataFilter(config); + return f === 'both' ? 'all' : f; +} + +function externalSyncEnabled(config) { + return probeScopeSetting(config) !== 'config'; +} + +// 本轮需要本地探测的目标(UR 镜像目标不在此列,它们由 UR 节点探测、这里只拉结果) +function localProbeTargets(config) { + return probeScopeSetting(config) === 'uptimerobot' ? [] : config.monitors; +} + /* ---------- 服务端更新节奏 ---------- */ // 快照里回报「数据最后变更时刻」与「服务端产出新数据的节奏」,供面板把倒计时对齐到服务端, @@ -677,6 +717,8 @@ function snapshot(runtime, demo = false) { updateIntervalSec: updateIntervalSec(config, filter), // 让面板与服务端共用同一份过滤口径,避免私有配置覆盖后两边不一致 dataFilter: filter, + // 探测范围(本地目标 / 外部数据源),面板据此显示「数据源」与是否停止本地探测 + probeScope: probeScopeSetting(config), stats: { total: entries.length, up, @@ -698,9 +740,14 @@ function snapshot(runtime, demo = false) { lastResult: runtime.notifyState?.lastResult ?? null, }; })(), - events: (runtime.events ?? []).slice(-EVENT_IN_SNAPSHOT).reverse().map((e) => (redact - ? { t: e.t, id: e.id, type: e.type, name: e.name, detail: e.type === 'down' ? '异常' : e.type === 'up' ? '恢复' : e.detail, maintenance: e.maintenance } - : e)), + // 只下发当前展示口径内目标的事件:切到 uptimerobot 口径后不该再看到「已经不管的目标」的旧事件 + events: (runtime.events ?? []) + .filter((e) => monitors[e.id]) + .slice(-EVENT_IN_SNAPSHOT) + .reverse() + .map((e) => (redact + ? { t: e.t, id: e.id, type: e.type, name: e.name, detail: e.type === 'down' ? '异常' : e.type === 'up' ? '恢复' : e.detail, maintenance: e.maintenance } + : e)), monitors, }; } @@ -987,7 +1034,7 @@ async function main() { } runtime.state = hydrate(runtime.config); // 启用 UptimeRobot 时先同步一次,保证首次渲染就有异地探测数据 - await syncUptimeRobot(runtime); + if (externalSyncEnabled(runtime.config)) await syncUptimeRobot(runtime); if (ONCE) { await probeDue(runtime, true); @@ -1024,7 +1071,7 @@ async function main() { }) .catch((err) => console.error(`[tick] ${err.stack || err}`)); // UptimeRobot 按自身 pollIntervalSec 拉取,失败不影响本地巡检 - if (Date.now() - runtime.ur.lastSyncMs >= uptimeRobotConfig(runtime.config).pollIntervalSec * 1000) { + if (externalSyncEnabled(runtime.config) && Date.now() - runtime.ur.lastSyncMs >= uptimeRobotConfig(runtime.config).pollIntervalSec * 1000) { syncUptimeRobot(runtime) .then(() => scheduleWrite(runtime)) .catch(() => { /* syncUptimeRobot 内部已记录错误 */ }); @@ -1041,7 +1088,17 @@ async function main() { console.log(`面板: http://localhost:${PORT}/`); console.log(`状态 API: http://localhost:${PORT}/api/status`); console.log(`兼容接口: http://localhost:${PORT}/api/v2/getMonitors (UptimeRobot getMonitors 只读兼容)`); - console.log(`监控目标: ${runtime.config.monitors.length} 个自建探测${runtime.ur.enabled ? ` + ${runtime.ur.count} 个 UptimeRobot 监控` : ''}(配置热重载已启用)`); + const scope = probeScopeSetting(runtime.config); + const localCount = localProbeTargets(runtime.config).length; + const scopeText = { + all: `全部(自建 ${localCount} 个${runtime.ur.enabled ? ` + UptimeRobot ${runtime.ur.count} 个` : ''})`, + config: `仅自建(${localCount} 个,不同步 UptimeRobot)`, + uptimerobot: `仅 UptimeRobot(${runtime.ur.count} 个,**不探测本地配置里的 ${runtime.config.monitors.length} 个目标**)`, + }[scope]; + console.log(`展示口径: ${dataFilter(runtime.config)} 探测范围: ${scope} —— ${scopeText}(settings.probeScope 可显式覆盖)`); + if (!localCount && !runtime.ur.enabled) { + console.warn('[warn] 当前没有任何数据源:本地探测已关闭且 UptimeRobot 未启用,面板将显示「等待检测」'); + } }); } diff --git a/public/assets/app.js b/public/assets/app.js index 15d6858..afbd743 100644 --- a/public/assets/app.js +++ b/public/assets/app.js @@ -1,4 +1,4 @@ -const VERSION = '0.11.0'; +const VERSION = '0.11.1'; const HB_LIMIT = 48; const UPTIME_DAYS = 90; const BROWSER_CONCURRENCY = 6; @@ -16,6 +16,7 @@ const state = { filter: 'all', view: 'cards', theme: 'default', + eventsExpanded: false, refreshTimer: null, nextRefreshAt: 0, loading: false, @@ -243,18 +244,26 @@ function latTrendBar(lat) { } // 最近事件区块(故障 / 恢复 / 证书阈值 / 维护窗口内的事件) +// 默认只列最近 EVENTS_COLLAPSED 条,其余折叠在「展开全部」后面,避免长列表把页面撑得过长 +const EVENTS_COLLAPSED = 8; + 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 all = state.status?.events ?? []; + box.hidden = all.length === 0; + empty.hidden = all.length > 0; + if (!all.length) return; + + const expanded = state.eventsExpanded === true; + const shown = expanded ? all : all.slice(0, EVENTS_COLLAPSED); const count = $('events-count'); - if (count) count.textContent = events.length ? `最近 ${events.length} 条` : ''; + if (count) count.textContent = all.length > shown.length ? `最近 ${shown.length} / ${all.length} 条` : `最近 ${all.length} 条`; + const label = { down: '故障', up: '恢复', tls: '证书', maintenance: '维护' }; - list.innerHTML = events.map((e) => { + list.innerHTML = shown.map((e) => { const maint = e.maintenance ? '维护' : ''; return `
暂无事件记录(故障、恢复与证书提醒会出现在这里)
@@ -67,7 +68,7 @@ - + diff --git a/public/status.json b/public/status.json index 22b6b20..73aad6e 100644 --- a/public/status.json +++ b/public/status.json @@ -1,21 +1,22 @@ { - "prober": "shiroko-monitor-node/0.10.0", + "prober": "shiroko-monitor-node/0.11.1", "demo": false, - "generatedAt": "2026-10-13T16:32:57.040Z", - "dataUpdatedAt": "2026-10-13T16:32:52.050Z", + "generatedAt": "2026-10-14T02:22:02.705Z", + "dataUpdatedAt": "2026-10-14T02:20:34.658Z", "updateIntervalSec": 300, "dataFilter": "uptimerobot", + "probeScope": "uptimerobot", "stats": { "total": 6, "up": 5, "down": 1, "pending": 0, - "avgLatency": 764, + "avgLatency": 654, "avgUptime90": 83.3, "memory": { - "rssMB": 66.8, - "heapUsedMB": 12.7, - "heapTotalMB": 16 + "rssMB": 57.2, + "heapUsedMB": 9, + "heapTotalMB": 10.5 } }, "sources": { @@ -23,10 +24,10 @@ "enabled": true, "ok": true, "error": null, - "lastSyncAt": "2026-10-13T16:30:34.184Z", + "lastSyncAt": "2026-10-14T02:20:34.658Z", "count": 6, "syncing": false, - "lastSyncMs": 1791909032527 + "lastSyncMs": 1791944433802 } }, "notify": { @@ -35,7 +36,7 @@ "quietHours": null, "failureThreshold": 2, "lastResult": { - "at": "2026-10-13T16:32:53.722Z", + "at": "2026-10-14T02:19:42.390Z", "suppressed": false, "skipped": "notify-disabled", "results": [] @@ -43,309 +44,200 @@ }, "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 站点", + "t": "2026-10-14T02:11:47.171Z", + "id": "ur-796219015", + "name": "狼的评论系统", "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", - "name": "Cloudflare 官网", - "type": "up", - "detail": "已恢复", - "maintenance": false - }, - { - "t": "2026-10-13T15:52:51.476Z", - "id": "cloudflare-site", - "name": "Cloudflare 官网", - "type": "down", - "detail": "ETIMEDOUT", - "maintenance": false - }, - { - "t": "2026-10-13T15:32:38.735Z", - "id": "google-dns-tcp", - "name": "Google DNS 53 端口", - "type": "down", - "detail": "TCP 连接超时", - "maintenance": false - }, - { - "t": "2026-10-13T15:32:38.735Z", - "id": "cloudflare-dns-tcp", - "name": "Cloudflare 443 端口", - "type": "down", - "detail": "TCP 连接超时", - "maintenance": false - }, - { - "t": "2026-10-13T15:32:34.170Z", - "id": "badssl-expired", - "name": "BadSSL 过期证书(演示离线)", - "type": "down", - "detail": "CERT_HAS_EXPIRED", - "maintenance": false - }, - { - "t": "2026-10-13T15:32:34.129Z", - "id": "badssl-expired", - "name": "BadSSL 过期证书(演示离线)", - "type": "tls", - "detail": "证书剩余 -4202 天(阈值 3 天)", - "maintenance": false - }, - { - "t": "2026-10-13T15:32:34.129Z", - "id": "badssl-expired", - "name": "BadSSL 过期证书(演示离线)", - "type": "tls", - "detail": "证书剩余 -4202 天(阈值 7 天)", - "maintenance": false - }, - { - "t": "2026-10-13T15:32:34.129Z", - "id": "badssl-expired", - "name": "BadSSL 过期证书(演示离线)", - "type": "tls", - "detail": "证书剩余 -4202 天(阈值 14 天)", + "detail": "UptimeRobot:403 Forbidden", "maintenance": false } ], "monitors": { "ur-792059439": { "status": "up", - "latency": 396, + "latency": 295, "code": null, "detail": "UptimeRobot:运行正常", - "lastCheck": "2026-10-13T16:26:00.000Z", + "lastCheck": "2026-10-14T02:15:00.000Z", "history": [ { - "t": "2026-10-12T17:40:00.000Z", + "t": "2026-10-13T03:30:00.000Z", "ok": true, - "ms": 304 + "ms": 196 }, { - "t": "2026-10-12T18:20:00.000Z", + "t": "2026-10-13T04:10:00.000Z", + "ok": true, + "ms": 348 + }, + { + "t": "2026-10-13T04:50:00.000Z", + "ok": true, + "ms": 305 + }, + { + "t": "2026-10-13T05:30:00.000Z", + "ok": true, + "ms": 287 + }, + { + "t": "2026-10-13T06:10:00.000Z", + "ok": true, + "ms": 290 + }, + { + "t": "2026-10-13T06:50:00.000Z", + "ok": true, + "ms": 261 + }, + { + "t": "2026-10-13T07:30:00.000Z", "ok": true, "ms": 323 }, { - "t": "2026-10-12T19:00:00.000Z", + "t": "2026-10-13T08:10:00.000Z", "ok": true, - "ms": 328 + "ms": 280 }, { - "t": "2026-10-12T19:40:00.000Z", + "t": "2026-10-13T08:50:00.000Z", "ok": true, - "ms": 304 + "ms": 372 }, { - "t": "2026-10-12T20:20:00.000Z", + "t": "2026-10-13T09:30:00.000Z", "ok": true, - "ms": 265 + "ms": 263 }, { - "t": "2026-10-12T21:00:00.000Z", + "t": "2026-10-13T10:15:00.000Z", "ok": true, - "ms": 294 + "ms": 307 }, { - "t": "2026-10-12T21:40:00.000Z", + "t": "2026-10-13T10:55:00.000Z", "ok": true, "ms": 379 }, { - "t": "2026-10-12T22:20:00.000Z", + "t": "2026-10-13T11:35:00.000Z", "ok": true, - "ms": 176 + "ms": 244 }, { - "t": "2026-10-12T23:00:00.000Z", + "t": "2026-10-13T12:15:00.000Z", "ok": true, - "ms": 325 + "ms": 337 }, { - "t": "2026-10-12T23:40:00.000Z", + "t": "2026-10-13T12:55:00.000Z", "ok": true, - "ms": 192 + "ms": 392 }, { - "t": "2026-10-13T00:20:00.000Z", + "t": "2026-10-13T13:35:00.000Z", "ok": true, - "ms": 342 + "ms": 249 }, { - "t": "2026-10-13T01:00:00.000Z", + "t": "2026-10-13T14:15:00.000Z", "ok": true, - "ms": 252 + "ms": 549 }, { - "t": "2026-10-13T01:40:00.000Z", + "t": "2026-10-13T14:55:00.000Z", "ok": true, - "ms": 164 + "ms": 231 }, { - "t": "2026-10-13T02:20:00.000Z", + "t": "2026-10-13T15:40:00.000Z", "ok": true, - "ms": 199 + "ms": 251 }, { - "t": "2026-10-13T03:00:00.000Z", + "t": "2026-10-13T16:20:00.000Z", "ok": true, - "ms": 216 + "ms": 482 }, { - "t": "2026-10-13T03:40:00.000Z", + "t": "2026-10-13T17:00:00.000Z", "ok": true, - "ms": 160 + "ms": 266 }, { - "t": "2026-10-13T04:20:00.000Z", + "t": "2026-10-13T17:40:00.000Z", "ok": true, - "ms": 435 + "ms": 249 }, { - "t": "2026-10-13T05:00:00.000Z", + "t": "2026-10-13T18:20:00.000Z", "ok": true, - "ms": 281 + "ms": 305 }, { - "t": "2026-10-13T05:40:00.000Z", + "t": "2026-10-13T19:00:00.000Z", "ok": true, - "ms": 285 + "ms": 479 }, { - "t": "2026-10-13T06:20:00.000Z", + "t": "2026-10-13T19:40:00.000Z", "ok": true, - "ms": 270 + "ms": 378 }, { - "t": "2026-10-13T07:00:00.000Z", + "t": "2026-10-13T20:20:00.000Z", "ok": true, - "ms": 235 + "ms": 372 }, { - "t": "2026-10-13T07:40:00.000Z", + "t": "2026-10-13T21:00:00.000Z", "ok": true, - "ms": 271 + "ms": 276 }, { - "t": "2026-10-13T08:20:00.000Z", + "t": "2026-10-13T21:40:00.000Z", "ok": true, - "ms": 417 + "ms": 304 }, { - "t": "2026-10-13T09:00:00.000Z", + "t": "2026-10-13T22:20:00.000Z", "ok": true, - "ms": 396 + "ms": 202 }, { - "t": "2026-10-13T09:40:00.000Z", + "t": "2026-10-13T23:00:00.000Z", "ok": true, - "ms": 296 + "ms": 228 }, { - "t": "2026-10-13T10:20:00.000Z", + "t": "2026-10-13T23:40:00.000Z", "ok": true, - "ms": 183 + "ms": 316 }, { - "t": "2026-10-13T11:00:00.000Z", + "t": "2026-10-14T00:20:00.000Z", "ok": true, - "ms": 413 + "ms": 356 }, { - "t": "2026-10-13T11:40:00.000Z", + "t": "2026-10-14T01:35:00.000Z", "ok": true, - "ms": 232 + "ms": 343 }, { - "t": "2026-10-13T12:20:00.000Z", + "t": "2026-10-14T02:15: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": 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-14": { - "total": 6, - "up": 6, - "latSum": 3003, - "latCount": 6, - "latMax": 538 + "total": 1, + "up": 1, + "latSum": 295, + "latCount": 1, + "latMax": 295 } }, "meta": { @@ -370,98 +262,23 @@ "90": 0 }, "lat": { - "avg": 501, - "max": 538, + "avg": 295, + "max": 295, "days": [ { "key": "2026-10-14", - "avg": 501 + "avg": 295 } ] } }, "ur-795497022": { "status": "up", - "latency": 765, + "latency": 816, "code": null, "detail": "UptimeRobot:运行正常", - "lastCheck": "2026-10-13T16:25:00.000Z", + "lastCheck": "2026-10-14T02:19:00.000Z", "history": [ - { - "t": "2026-10-12T17:35:00.000Z", - "ok": true, - "ms": 714 - }, - { - "t": "2026-10-12T18:15:00.000Z", - "ok": true, - "ms": 712 - }, - { - "t": "2026-10-12T18:55:00.000Z", - "ok": true, - "ms": 728 - }, - { - "t": "2026-10-12T19:35:00.000Z", - "ok": true, - "ms": 739 - }, - { - "t": "2026-10-12T20:15:00.000Z", - "ok": true, - "ms": 751 - }, - { - "t": "2026-10-12T20:55:00.000Z", - "ok": true, - "ms": 665 - }, - { - "t": "2026-10-12T21:35:00.000Z", - "ok": true, - "ms": 632 - }, - { - "t": "2026-10-12T22:15:00.000Z", - "ok": true, - "ms": 754 - }, - { - "t": "2026-10-12T22:55:00.000Z", - "ok": true, - "ms": 628 - }, - { - "t": "2026-10-12T23:35:00.000Z", - "ok": true, - "ms": 706 - }, - { - "t": "2026-10-13T00:15:00.000Z", - "ok": true, - "ms": 640 - }, - { - "t": "2026-10-13T00:55:00.000Z", - "ok": true, - "ms": 732 - }, - { - "t": "2026-10-13T01:35:00.000Z", - "ok": true, - "ms": 602 - }, - { - "t": "2026-10-13T02:15:00.000Z", - "ok": true, - "ms": 656 - }, - { - "t": "2026-10-13T02:55:00.000Z", - "ok": true, - "ms": 653 - }, { "t": "2026-10-13T03:35:00.000Z", "ok": true, @@ -548,23 +365,103 @@ "ms": 808 }, { - "t": "2026-10-13T15:48:00.000Z", + "t": "2026-10-13T15:00:00.000Z", "ok": true, - "ms": 782 + "ms": 842 }, { - "t": "2026-10-13T16:25:00.000Z", + "t": "2026-10-13T15:40:00.000Z", "ok": true, - "ms": 765 + "ms": 918 + }, + { + "t": "2026-10-13T16:20:00.000Z", + "ok": true, + "ms": 646 + }, + { + "t": "2026-10-13T17:00:00.000Z", + "ok": true, + "ms": 722 + }, + { + "t": "2026-10-13T17:40:00.000Z", + "ok": true, + "ms": 749 + }, + { + "t": "2026-10-13T18:20:00.000Z", + "ok": true, + "ms": 750 + }, + { + "t": "2026-10-13T19:00:00.000Z", + "ok": true, + "ms": 920 + }, + { + "t": "2026-10-13T19:40:00.000Z", + "ok": true, + "ms": 715 + }, + { + "t": "2026-10-13T20:20:00.000Z", + "ok": true, + "ms": 835 + }, + { + "t": "2026-10-13T21:00:00.000Z", + "ok": true, + "ms": 1038 + }, + { + "t": "2026-10-13T21:40:00.000Z", + "ok": true, + "ms": 1235 + }, + { + "t": "2026-10-13T22:20:00.000Z", + "ok": true, + "ms": 731 + }, + { + "t": "2026-10-13T23:00:00.000Z", + "ok": true, + "ms": 685 + }, + { + "t": "2026-10-13T23:40:00.000Z", + "ok": true, + "ms": 597 + }, + { + "t": "2026-10-14T00:20:00.000Z", + "ok": true, + "ms": 676 + }, + { + "t": "2026-10-14T01:14:00.000Z", + "ok": true, + "ms": 783 + }, + { + "t": "2026-10-14T01:46:00.000Z", + "ok": true, + "ms": 646 + }, + { + "t": "2026-10-14T02:19:00.000Z", + "ok": true, + "ms": 816 } ], "daily": { "2026-10-14": { - "total": 6, - "up": 6, - "latSum": 4408, - "latCount": 6, - "latMax": 868 + "total": 1, + "up": 1, + "latSum": 816, + "latCount": 1, + "latMax": 816 } }, "meta": { @@ -589,201 +486,206 @@ "90": 5432 }, "lat": { - "avg": 735, - "max": 868, + "avg": 816, + "max": 816, "days": [ { "key": "2026-10-14", - "avg": 735 + "avg": 816 } ] } }, "ur-795556828": { "status": "up", - "latency": 359, + "latency": 120, "code": null, "detail": "UptimeRobot:运行正常", - "lastCheck": "2026-10-13T16:26:00.000Z", + "lastCheck": "2026-10-14T02:16:00.000Z", "history": [ { - "t": "2026-10-12T17:40:00.000Z", + "t": "2026-10-13T03:00:00.000Z", "ok": true, - "ms": 320 + "ms": 409 }, { - "t": "2026-10-12T18:20:00.000Z", + "t": "2026-10-13T03:40:00.000Z", "ok": true, - "ms": 287 + "ms": 231 }, { - "t": "2026-10-12T19:00:00.000Z", + "t": "2026-10-13T04:20:00.000Z", "ok": true, - "ms": 283 + "ms": 250 }, { - "t": "2026-10-12T19:40:00.000Z", + "t": "2026-10-13T05:00:00.000Z", "ok": true, - "ms": 464 + "ms": 134 }, { - "t": "2026-10-12T20:20:00.000Z", + "t": "2026-10-13T05:40:00.000Z", "ok": true, - "ms": 187 + "ms": 193 }, { - "t": "2026-10-12T21:00:00.000Z", + "t": "2026-10-13T06:20:00.000Z", "ok": true, - "ms": 233 + "ms": 260 }, { - "t": "2026-10-12T21:40:00.000Z", + "t": "2026-10-13T07:00:00.000Z", "ok": true, - "ms": 213 + "ms": 254 }, { - "t": "2026-10-12T22:20:00.000Z", - "ok": true, - "ms": 235 - }, - { - "t": "2026-10-12T23:05:00.000Z", - "ok": true, - "ms": 241 - }, - { - "t": "2026-10-12T23:45:00.000Z", - "ok": true, - "ms": 230 - }, - { - "t": "2026-10-13T00:25:00.000Z", - "ok": true, - "ms": 244 - }, - { - "t": "2026-10-13T01:05:00.000Z", - "ok": true, - "ms": 133 - }, - { - "t": "2026-10-13T01:45:00.000Z", - "ok": true, - "ms": 127 - }, - { - "t": "2026-10-13T02:25:00.000Z", - "ok": true, - "ms": 182 - }, - { - "t": "2026-10-13T03:05:00.000Z", - "ok": true, - "ms": 413 - }, - { - "t": "2026-10-13T03:45:00.000Z", - "ok": true, - "ms": 229 - }, - { - "t": "2026-10-13T04:25:00.000Z", - "ok": true, - "ms": 255 - }, - { - "t": "2026-10-13T05:05:00.000Z", - "ok": true, - "ms": 150 - }, - { - "t": "2026-10-13T05:45:00.000Z", - "ok": true, - "ms": 201 - }, - { - "t": "2026-10-13T06:25:00.000Z", - "ok": true, - "ms": 232 - }, - { - "t": "2026-10-13T07:05:00.000Z", - "ok": true, - "ms": 247 - }, - { - "t": "2026-10-13T07:45:00.000Z", + "t": "2026-10-13T07:40:00.000Z", "ok": true, "ms": 559 }, { - "t": "2026-10-13T08:25:00.000Z", + "t": "2026-10-13T08:20:00.000Z", "ok": true, - "ms": 219 + "ms": 205 }, { - "t": "2026-10-13T09:05:00.000Z", + "t": "2026-10-13T09:00:00.000Z", "ok": true, - "ms": 230 + "ms": 264 }, { - "t": "2026-10-13T09:45:00.000Z", + "t": "2026-10-13T09:40:00.000Z", "ok": true, - "ms": 432 + "ms": 305 }, { - "t": "2026-10-13T10:25:00.000Z", + "t": "2026-10-13T10:20:00.000Z", "ok": true, - "ms": 341 + "ms": 420 }, { - "t": "2026-10-13T11:05:00.000Z", + "t": "2026-10-13T11:00:00.000Z", "ok": true, - "ms": 233 + "ms": 227 }, { - "t": "2026-10-13T11:45:00.000Z", + "t": "2026-10-13T11:40:00.000Z", "ok": true, - "ms": 396 + "ms": 389 }, { - "t": "2026-10-13T12:25:00.000Z", + "t": "2026-10-13T12:20:00.000Z", "ok": true, - "ms": 308 + "ms": 302 }, { - "t": "2026-10-13T13:05:00.000Z", + "t": "2026-10-13T13:00:00.000Z", "ok": true, - "ms": 344 + "ms": 375 }, { - "t": "2026-10-13T13:45:00.000Z", + "t": "2026-10-13T13:40:00.000Z", "ok": true, - "ms": 278 + "ms": 287 }, { - "t": "2026-10-13T14:25:00.000Z", + "t": "2026-10-13T14:20:00.000Z", "ok": true, - "ms": 437 + "ms": 352 }, { - "t": "2026-10-13T15:50:00.000Z", + "t": "2026-10-13T15:00:00.000Z", "ok": true, - "ms": 245 + "ms": 404 }, { - "t": "2026-10-13T16:26:00.000Z", + "t": "2026-10-13T15:40:00.000Z", "ok": true, - "ms": 359 + "ms": 323 + }, + { + "t": "2026-10-13T16:20:00.000Z", + "ok": true, + "ms": 311 + }, + { + "t": "2026-10-13T17:00:00.000Z", + "ok": true, + "ms": 376 + }, + { + "t": "2026-10-13T17:40:00.000Z", + "ok": true, + "ms": 413 + }, + { + "t": "2026-10-13T18:20:00.000Z", + "ok": true, + "ms": 386 + }, + { + "t": "2026-10-13T19:00:00.000Z", + "ok": true, + "ms": 302 + }, + { + "t": "2026-10-13T19:40:00.000Z", + "ok": true, + "ms": 376 + }, + { + "t": "2026-10-13T20:20:00.000Z", + "ok": true, + "ms": 266 + }, + { + "t": "2026-10-13T21:00:00.000Z", + "ok": true, + "ms": 475 + }, + { + "t": "2026-10-13T21:40:00.000Z", + "ok": true, + "ms": 354 + }, + { + "t": "2026-10-13T22:20:00.000Z", + "ok": true, + "ms": 239 + }, + { + "t": "2026-10-13T23:00:00.000Z", + "ok": true, + "ms": 225 + }, + { + "t": "2026-10-13T23:40:00.000Z", + "ok": true, + "ms": 243 + }, + { + "t": "2026-10-14T00:20:00.000Z", + "ok": true, + "ms": 319 + }, + { + "t": "2026-10-14T01:40:00.000Z", + "ok": true, + "ms": 382 + }, + { + "t": "2026-10-14T02:16:00.000Z", + "ok": true, + "ms": 120 } ], "daily": { "2026-10-14": { - "total": 6, - "up": 6, - "latSum": 1935, - "latCount": 6, - "latMax": 359 + "total": 1, + "up": 1, + "latSum": 120, + "latCount": 1, + "latMax": 120 } }, "meta": { @@ -808,88 +710,23 @@ "90": 0 }, "lat": { - "avg": 323, - "max": 359, + "avg": 120, + "max": 120, "days": [ { "key": "2026-10-14", - "avg": 323 + "avg": 120 } ] } }, "ur-792059414": { "status": "up", - "latency": 2003, + "latency": 1760, "code": null, "detail": "UptimeRobot:运行正常", - "lastCheck": "2026-10-13T16:28:00.000Z", + "lastCheck": "2026-10-14T02:18:00.000Z", "history": [ - { - "t": "2026-10-12T17:55:00.000Z", - "ok": true, - "ms": 2291 - }, - { - "t": "2026-10-12T18:40:00.000Z", - "ok": true, - "ms": 2251 - }, - { - "t": "2026-10-12T19:25:00.000Z", - "ok": true, - "ms": 2178 - }, - { - "t": "2026-10-12T20:10:00.000Z", - "ok": true, - "ms": 2123 - }, - { - "t": "2026-10-12T20:55:00.000Z", - "ok": true, - "ms": 2087 - }, - { - "t": "2026-10-12T21:40:00.000Z", - "ok": true, - "ms": 1870 - }, - { - "t": "2026-10-12T22:20:00.000Z", - "ok": true, - "ms": 1662 - }, - { - "t": "2026-10-12T23:00:00.000Z", - "ok": true, - "ms": 1859 - }, - { - "t": "2026-10-12T23:40:00.000Z", - "ok": true, - "ms": 3138 - }, - { - "t": "2026-10-13T00:20:00.000Z", - "ok": true, - "ms": 1681 - }, - { - "t": "2026-10-13T01:00:00.000Z", - "ok": true, - "ms": 2312 - }, - { - "t": "2026-10-13T01:40:00.000Z", - "ok": true, - "ms": 2200 - }, - { - "t": "2026-10-13T02:25:00.000Z", - "ok": true, - "ms": 1924 - }, { "t": "2026-10-13T03:10:00.000Z", "ok": true, @@ -961,38 +798,103 @@ "ms": 2938 }, { - "t": "2026-10-13T13:35:00.000Z", + "t": "2026-10-13T13:40:00.000Z", "ok": true, - "ms": 3044 + "ms": 3146 }, { - "t": "2026-10-13T14:15:00.000Z", + "t": "2026-10-13T14:25:00.000Z", "ok": true, - "ms": 2092 + "ms": 2125 }, { - "t": "2026-10-13T14:55:00.000Z", + "t": "2026-10-13T15:05:00.000Z", "ok": true, - "ms": 2732 + "ms": 2892 }, { - "t": "2026-10-13T15:48:00.000Z", + "t": "2026-10-13T15:50:00.000Z", "ok": true, - "ms": 2321 + "ms": 2097 }, { - "t": "2026-10-13T16:28:00.000Z", + "t": "2026-10-13T16:35:00.000Z", "ok": true, - "ms": 2003 + "ms": 2206 + }, + { + "t": "2026-10-13T17:20:00.000Z", + "ok": true, + "ms": 2293 + }, + { + "t": "2026-10-13T18:05:00.000Z", + "ok": true, + "ms": 1347 + }, + { + "t": "2026-10-13T18:50:00.000Z", + "ok": true, + "ms": 1656 + }, + { + "t": "2026-10-13T19:35:00.000Z", + "ok": true, + "ms": 2883 + }, + { + "t": "2026-10-13T20:20:00.000Z", + "ok": true, + "ms": 2649 + }, + { + "t": "2026-10-13T21:00:00.000Z", + "ok": true, + "ms": 3079 + }, + { + "t": "2026-10-13T21:40:00.000Z", + "ok": true, + "ms": 2829 + }, + { + "t": "2026-10-13T22:20:00.000Z", + "ok": true, + "ms": 1984 + }, + { + "t": "2026-10-13T23:00:00.000Z", + "ok": true, + "ms": 1853 + }, + { + "t": "2026-10-13T23:40:00.000Z", + "ok": true, + "ms": 2628 + }, + { + "t": "2026-10-14T00:20:00.000Z", + "ok": true, + "ms": 2416 + }, + { + "t": "2026-10-14T01:38:00.000Z", + "ok": true, + "ms": 3054 + }, + { + "t": "2026-10-14T02:18:00.000Z", + "ok": true, + "ms": 1760 } ], "daily": { "2026-10-14": { - "total": 6, - "up": 6, - "latSum": 12460, - "latCount": 6, - "latMax": 2270 + "total": 1, + "up": 1, + "latSum": 1760, + "latCount": 1, + "latMax": 1760 } }, "meta": { @@ -1017,201 +919,201 @@ "90": 7593 }, "lat": { - "avg": 2077, - "max": 2270, + "avg": 1760, + "max": 1760, "days": [ { "key": "2026-10-14", - "avg": 2077 + "avg": 1760 } ] } }, "ur-798788113": { "status": "up", - "latency": 297, + "latency": 281, "code": null, "detail": "UptimeRobot:运行正常", - "lastCheck": "2026-10-13T16:27:00.000Z", + "lastCheck": "2026-10-14T02:16:00.000Z", "history": [ { - "t": "2026-10-12T17:30:00.000Z", + "t": "2026-10-13T03:25:00.000Z", "ok": true, - "ms": 320 + "ms": 446 }, { - "t": "2026-10-12T18:10:00.000Z", - "ok": true, - "ms": 404 - }, - { - "t": "2026-10-12T18:50:00.000Z", - "ok": true, - "ms": 343 - }, - { - "t": "2026-10-12T19:30:00.000Z", - "ok": true, - "ms": 476 - }, - { - "t": "2026-10-12T20:10:00.000Z", - "ok": true, - "ms": 285 - }, - { - "t": "2026-10-12T20:50:00.000Z", - "ok": true, - "ms": 449 - }, - { - "t": "2026-10-12T21:30:00.000Z", - "ok": true, - "ms": 332 - }, - { - "t": "2026-10-12T22:10:00.000Z", - "ok": true, - "ms": 450 - }, - { - "t": "2026-10-12T22:50:00.000Z", - "ok": true, - "ms": 231 - }, - { - "t": "2026-10-12T23:30:00.000Z", - "ok": true, - "ms": 249 - }, - { - "t": "2026-10-13T00:10:00.000Z", - "ok": true, - "ms": 286 - }, - { - "t": "2026-10-13T00:50:00.000Z", - "ok": true, - "ms": 223 - }, - { - "t": "2026-10-13T01:30:00.000Z", - "ok": true, - "ms": 386 - }, - { - "t": "2026-10-13T02:10:00.000Z", - "ok": true, - "ms": 511 - }, - { - "t": "2026-10-13T02:50:00.000Z", - "ok": true, - "ms": 292 - }, - { - "t": "2026-10-13T03:30:00.000Z", - "ok": true, - "ms": 435 - }, - { - "t": "2026-10-13T04:15:00.000Z", + "t": "2026-10-13T04:10:00.000Z", "ok": true, "ms": 400 }, { - "t": "2026-10-13T04:55:00.000Z", + "t": "2026-10-13T04:50:00.000Z", "ok": true, - "ms": 462 + "ms": 422 }, { - "t": "2026-10-13T05:35:00.000Z", + "t": "2026-10-13T05:30:00.000Z", "ok": true, - "ms": 377 + "ms": 449 }, { - "t": "2026-10-13T06:15:00.000Z", + "t": "2026-10-13T06:10:00.000Z", "ok": true, - "ms": 396 + "ms": 386 }, { - "t": "2026-10-13T06:55:00.000Z", + "t": "2026-10-13T06:50:00.000Z", "ok": true, - "ms": 310 + "ms": 311 }, { - "t": "2026-10-13T07:35:00.000Z", + "t": "2026-10-13T07:30:00.000Z", "ok": true, - "ms": 336 + "ms": 384 }, { - "t": "2026-10-13T08:15:00.000Z", + "t": "2026-10-13T08:10:00.000Z", "ok": true, - "ms": 379 + "ms": 390 }, { - "t": "2026-10-13T08:55:00.000Z", + "t": "2026-10-13T08:50:00.000Z", "ok": true, - "ms": 476 + "ms": 461 }, { - "t": "2026-10-13T09:35:00.000Z", + "t": "2026-10-13T09:30:00.000Z", "ok": true, - "ms": 605 + "ms": 545 }, { - "t": "2026-10-13T10:15:00.000Z", + "t": "2026-10-13T10:10:00.000Z", "ok": true, "ms": 456 }, { - "t": "2026-10-13T10:55:00.000Z", + "t": "2026-10-13T10:50:00.000Z", "ok": true, - "ms": 420 + "ms": 394 }, { - "t": "2026-10-13T11:35:00.000Z", + "t": "2026-10-13T11:30:00.000Z", "ok": true, - "ms": 878 + "ms": 398 }, { - "t": "2026-10-13T12:15:00.000Z", + "t": "2026-10-13T12:10:00.000Z", "ok": true, - "ms": 530 + "ms": 588 }, { - "t": "2026-10-13T12:55:00.000Z", + "t": "2026-10-13T12:50:00.000Z", "ok": true, - "ms": 517 + "ms": 430 }, { - "t": "2026-10-13T13:35:00.000Z", + "t": "2026-10-13T13:30:00.000Z", "ok": true, - "ms": 491 + "ms": 502 }, { - "t": "2026-10-13T14:20:00.000Z", + "t": "2026-10-13T14:15:00.000Z", "ok": true, - "ms": 429 + "ms": 355 }, { - "t": "2026-10-13T15:46:00.000Z", + "t": "2026-10-13T14:55:00.000Z", "ok": true, - "ms": 340 + "ms": 358 }, { - "t": "2026-10-13T16:27:00.000Z", + "t": "2026-10-13T15:35:00.000Z", "ok": true, - "ms": 297 + "ms": 385 + }, + { + "t": "2026-10-13T16:15:00.000Z", + "ok": true, + "ms": 342 + }, + { + "t": "2026-10-13T16:55:00.000Z", + "ok": true, + "ms": 428 + }, + { + "t": "2026-10-13T17:35:00.000Z", + "ok": true, + "ms": 508 + }, + { + "t": "2026-10-13T18:15:00.000Z", + "ok": true, + "ms": 602 + }, + { + "t": "2026-10-13T18:55:00.000Z", + "ok": true, + "ms": 363 + }, + { + "t": "2026-10-13T19:40:00.000Z", + "ok": true, + "ms": 1380 + }, + { + "t": "2026-10-13T20:20:00.000Z", + "ok": true, + "ms": 512 + }, + { + "t": "2026-10-13T21:00:00.000Z", + "ok": true, + "ms": 394 + }, + { + "t": "2026-10-13T21:40:00.000Z", + "ok": true, + "ms": 238 + }, + { + "t": "2026-10-13T22:20:00.000Z", + "ok": true, + "ms": 323 + }, + { + "t": "2026-10-13T23:00:00.000Z", + "ok": true, + "ms": 353 + }, + { + "t": "2026-10-13T23:40:00.000Z", + "ok": true, + "ms": 179 + }, + { + "t": "2026-10-14T00:20:00.000Z", + "ok": true, + "ms": 263 + }, + { + "t": "2026-10-14T01:36:00.000Z", + "ok": true, + "ms": 383 + }, + { + "t": "2026-10-14T02:16:00.000Z", + "ok": true, + "ms": 281 } ], "daily": { "2026-10-14": { - "total": 6, - "up": 6, - "latSum": 1914, - "latCount": 6, - "latMax": 352 + "total": 1, + "up": 1, + "latSum": 281, + "latCount": 1, + "latMax": 281 } }, "meta": { @@ -1236,23 +1138,28 @@ "90": 0 }, "lat": { - "avg": 319, - "max": 352, + "avg": 281, + "max": 281, "days": [ { "key": "2026-10-14", - "avg": 319 + "avg": 281 } ] } }, "ur-796219015": { "status": "down", - "latency": 123, + "latency": 185, "code": 403, "detail": "UptimeRobot:403 Forbidden", "lastCheck": "2026-05-07T07:54:09.000Z", "history": [ + { + "t": "2026-10-13T19:25:00.000Z", + "ok": true, + "ms": 185 + }, { "t": "2026-05-07T07:54:09.000Z", "ok": false, @@ -1261,7 +1168,7 @@ ], "daily": { "2026-10-14": { - "total": 6, + "total": 1, "up": 0 } }, diff --git a/scripts/verify-notify.cjs b/scripts/verify-notify.cjs index 7286094..ba416cd 100644 --- a/scripts/verify-notify.cjs +++ b/scripts/verify-notify.cjs @@ -298,6 +298,9 @@ async function main() { check('告警邮件含目标名称与详情', /必然离线的 TCP/.test(downDecoded.body) && /【故障】/.test(downDecoded.subject), `主题「${downDecoded.subject}」正文 ${downDecoded.body.length} 字符`); const events2 = readEvents(dataDir); check('事件时间线记录 down', events2.some((e) => e.type === 'down' && e.id === 'down-tcp'), JSON.stringify(events2.map((e) => e.type))); + // 去抖计数必须与「探测轮次」绑定:评估每秒跑一次,不能每秒累加 + const ns2 = JSON.parse(fs.readFileSync(path.join(dataDir, 'notify-state.json'), 'utf8')); + check('连续失败计数=探测轮数(不被每秒评估放大)', ns2.monitors['down-tcp'].failStreak === 2, `failStreak=${ns2.monitors['down-tcp'].failStreak},期望 2`); await runOnce(['--once', ...common]); const mailAfter3 = smtp.received.length - beforeMail; @@ -335,6 +338,33 @@ async function main() { const snap2 = JSON.parse(fs.readFileSync(outFile2, 'utf8')); check('快照中目标带维护标记', snap2.monitors['down-tcp'].maintenance?.note === '计划维护', ''); + /* ---------- D3. 探测范围可选(probeScope) ---------- */ + console.log('\n[D3] 探测范围(probeScope)'); + const tmp3 = fs.mkdtempSync(path.join(os.tmpdir(), 'shiroko-scope-')); + fs.cpSync(path.join(ROOT, 'public'), tmp3, { recursive: true }); + const { file: file3 } = writeConfig(tmp3); + const scopeCfg = JSON.parse(fs.readFileSync(file3, 'utf8')); + scopeCfg.settings.dataFilter = 'uptimerobot'; // 只展示 UR 口径 + scopeCfg.settings.uptimeRobot = { enabled: false }; // 本测试不连 UR,只看本地探测是否发生 + scopeCfg.monitors.push({ id: 'local-must-not-run', name: '不该被探测的本地目标', group: '测试', type: 'tcp', host: '127.0.0.1', port: 1, interval: 60, timeout: 3 }); + fs.writeFileSync(file3, `${JSON.stringify(scopeCfg, null, 2)}\n`); + const scopeArgs = (cfgFile, out, data) => ['--once', `--config=${cfgFile}`, `--local-config=${cfgFile}`, `--out=${out}`, `--data-dir=${data}`]; + + const outA = path.join(tmp3, 'a.json'); + const resA = await runOnce(scopeArgs(file3, outA, path.join(tmp3, 'da'))); + const snapA = JSON.parse(fs.readFileSync(outA, 'utf8')); + check('dataFilter=uptimerobot 时探测范围自动跟随', snapA.probeScope === 'uptimerobot', `probeScope=${snapA.probeScope}`); + check('跟随口径时不再探测本地配置目标', !/local-must-not-run/.test(resA.out), resA.out.split('\n').filter((l) => /\[up\]|\[down\]/.test(l)).join(' | ').slice(0, 80) || '本地探测日志为空'); + + // 对照:显式 probeScope=all 时应恢复本地探测(证明是「可选」而非「砍掉」) + scopeCfg.settings.probeScope = 'all'; + fs.writeFileSync(file3, `${JSON.stringify(scopeCfg, null, 2)}\n`); + const outB = path.join(tmp3, 'b.json'); + const resB = await runOnce(scopeArgs(file3, outB, path.join(tmp3, 'db'))); + const snapB = JSON.parse(fs.readFileSync(outB, 'utf8')); + check('显式 probeScope=all 时恢复本地探测', snapB.probeScope === 'all' && /local-must-not-run/.test(resB.out), `probeScope=${snapB.probeScope}`); + check('探测范围与展示口径解耦(all + uptimerobot 展示)', snapB.dataFilter === 'uptimerobot' && snapB.probeScope === 'all', `filter=${snapB.dataFilter} scope=${snapB.probeScope}`); + /* ---------- E. 证书 ---------- */ console.log('\n[E] 证书'); check('tlsTargetOf 识别 https 目标', tlsTargetOf({ type: 'http', url: 'https://www.example.com:8443/x' }).port === 8443, ''); diff --git a/scripts/verify-views.cjs b/scripts/verify-views.cjs index a9c522e..47cccf2 100644 --- a/scripts/verify-views.cjs +++ b/scripts/verify-views.cjs @@ -299,6 +299,65 @@ async function main() { 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)); + + // ---- 极端内容:表格不得把数据截断,同时不得出现横向滚动 ---- + const statusPath = path.join(tmpDir, 'status.json'); + const snap = JSON.parse(fs.readFileSync(statusPath, 'utf8')); + const ids = Object.keys(snap.monitors); + let injected = 0; + for (const id of ids.slice(0, 2)) { + snap.monitors[id].name = `这是一个相当长的监控目标名称用于验证表格完整显示-${id}`; + injected++; + } + // 注入事件,验证事件区块的时间线样式与折叠 + snap.events = Array.from({ length: 12 }, (_, i) => ({ + t: new Date(Date.now() - i * 60000).toISOString(), + id: ids[0], + name: snap.monitors[ids[0]].name, + type: i % 2 ? 'down' : 'up', + detail: i % 2 ? 'ETIMEDOUT' : '已恢复', + maintenance: false, + })); + fs.writeFileSync(statusPath, JSON.stringify(snap)); + await dp.goto('http://localhost:8894/?source=static&view=table', { waitUntil: 'domcontentloaded' }); + await dp.waitForSelector('table.monitor-table tbody tr[data-status]', { timeout: 20000 }); + await dp.waitForTimeout(900); + const clip = await dp.evaluate(() => { + let clipped = 0; + let worst = 0; + for (const tr of document.querySelectorAll('tr[data-status]')) { + for (const td of tr.querySelectorAll('td')) { + const d = td.scrollWidth - td.clientWidth; + if (d > 1) { clipped++; worst = Math.max(worst, d); } + } + } + const wrap = document.querySelector('.table-wrap'); + return { clipped, worst, pageOverflow: document.documentElement.scrollWidth - document.documentElement.clientWidth, tableOverflow: wrap ? wrap.scrollWidth - wrap.clientWidth : 0 }; + }); + check(`长名称/长地址不再被截断(注入 ${injected} 个)`, clip.clipped === 0, `截断单元格 ${clip.clipped} 个,最大 ${clip.worst}px`); + check('表格仍无横向滚动', clip.pageOverflow <= 0 && clip.tableOverflow <= 0, `页面 ${clip.pageOverflow} / 表格 ${clip.tableOverflow}`); + const evr = await dp.evaluate(() => { + const ev = document.querySelector('.ev'); + const cs = ev ? getComputedStyle(ev) : null; + const dot = ev ? getComputedStyle(ev, '::before') : null; + const more = document.getElementById('events-more'); + return { + shown: document.querySelectorAll('.ev').length, + grid: cs?.display ?? 'none', + dotVisible: dot ? dot.width !== '0px' && dot.backgroundColor !== 'rgba(0, 0, 0, 0)' : false, + leftBorder: cs?.borderLeftWidth ?? '', + moreHidden: more?.hidden ?? true, + moreText: more?.textContent ?? '', + count: document.getElementById('events-count')?.textContent ?? '', + }; + }); + check('事件区块使用时间线布局(grid + 圆点)', evr.grid === 'grid' && evr.dotVisible === true, `display=${evr.grid} 圆点=${evr.dotVisible}`); + check('事件行按类型着色(左描边)', evr.leftBorder === '3px', evr.leftBorder); + check('事件默认折叠为 8 条并可展开', evr.shown === 8 && evr.moreHidden === false && /展开全部 12 条/.test(evr.moreText), `显示 ${evr.shown} 条,按钮「${evr.moreText}」,计数「${evr.count}」`); + await dp.click('#events-more'); + await dp.waitForTimeout(300); + const evExpanded = await dp.evaluate(() => ({ shown: document.querySelectorAll('.ev').length, text: document.getElementById('events-more').textContent })); + check('展开后显示全部事件', evExpanded.shown === 12 && evExpanded.text === '收起', `显示 ${evExpanded.shown} 条`); await dp.close(); await new Promise((r) => demoServer.close(r)); } finally {