diff --git a/README.md b/README.md index 078ae34..fd72ab4 100644 --- a/README.md +++ b/README.md @@ -524,6 +524,8 @@ node prober/node/index.js ### 面板部署在别处(跨域只读) +> 完整步骤见 **[docs/DEPLOY-PAGES.md](docs/DEPLOY-PAGES.md)**(GitHub Pages 面板 + 本地探针的部署指南,含常见问题排查) + 面板与探针可以放在两台机器上:面板放 GitHub Pages / Cloudflare(静态托管),探针放你家里或内网的机器上持续探测。两项配置: | 配置 | 作用 | diff --git a/docs/DEPLOY-PAGES.md b/docs/DEPLOY-PAGES.md new file mode 100644 index 0000000..a58aa82 --- /dev/null +++ b/docs/DEPLOY-PAGES.md @@ -0,0 +1,180 @@ +# 部署指南:GitHub Pages 面板 + 本地探针 + +面向的场景:**面板托管在 GitHub Pages(公开、免运维),探测跑在自己家里/内网的机器上**。 + +``` +┌─────────────────────────────┐ ┌──────────────────────────────────┐ +│ GitHub Pages(静态托管) │ HTTPS │ 你自己的机器(内网/家用) │ +│ ├─ index.html + assets/ │◄────────┤ ├─ Node 探针:持续探测 │ +│ └─ 读 status.json 取配置 │ fetch │ ├─ /admin 管理端(2FA 保护) │ +│ │ │ └─ 写 local.monitors.json │ +└─────────────────────────────┘ └──────────────────────────────────┘ +``` + +为什么面板不能直接读本地探针?因为 GitHub Pages 是**纯静态**的,只能读仓库里的文件。 +数据要上去,靠两件事:**跨域读接口**(本指南的方式)或 **Actions 提交快照**(下文备选方案)。 + +--- + +## 一、先决条件 + +| 条件 | 说明 | +| --- | --- | +| **本地探针有 HTTPS 地址** | Pages 强制 HTTPS,浏览器禁止 HTTPS 页面请求 HTTP(混合内容)。用你自己的隧道 / 反代给探针套一层 HTTPS | +| Node.js ≥ 18| 零依赖,只需 Node 自带模块 | +| 一个 GitHub 仓库 | 公开仓库即可(Pages 免费额度对公开仓库无限) | + +> ⚠️ **混合内容是硬限制**:即使 CORS 配对了,只要探针是 `http://`,浏览器仍会直接拦截请求,控制台只留一行 `Mixed Content ... was blocked`。这一条没有绕过办法。 + +--- + +## 二、本地:跑探针 + +```bash +git clone <你的仓库> +cd shiroko-monitor + +# 私有配置(不入库):真实地址、API Key 等放这里 +cp local.monitors.json.example local.monitors.json # 或按下面提示自建 +node prober/node/index.js +``` + +启动后确认: + +``` +面板:http://localhost:8787/ +管理后台: http://localhost:8787/admin +``` + +### 让探针能被外部访问 + +默认它**只监听 `127.0.0.1`**。要让隧道/反代连进来,在 `local.monitors.json` 里打开: + +```jsonc +{ + "settings": { + "admin": { "allowRemote": true } // 监听 0.0.0.0 + } +} +``` + +> 打开后同一端口上的公开面板也会对外可达(只读,但目标名称/地址/可用率会暴露)。 +> 如果不希望这样,请在隧道那层按路径只放行 `/admin` 与 `/api/admin/*`。 + +### 绑定管理端 2FA(必做) + +管理端能改配置、发通知,必须先绑定两步验证: + +```bash +# 首次绑定需要这个口令(只从环境变量读,不写进任何文件) +export SHIROKO_ADMIN_SETUP_KEY='换成一串足够长的随机口令' +node prober/node/index.js +``` + +然后打开 `https://你的隧道地址/admin`,按提示扫码绑定、保存恢复码。 + +--- + +## 三、页面:部署到 GitHub Pages + +### 1. 开启 Pages + +仓库 **Settings → Pages → Build and deployment → Source** 选 **GitHub Actions**。 + +### 2. 推送代码 + +```bash +git push +``` + +### 3. 填两处配置(关键) + +打开 `https://你的隧道地址/admin`,在「口径与范围」区块填: + +| 字段 | 填什么 | +| --- | --- | +| **面板上的管理入口链接** | `https://你的隧道地址/admin` —— Pages 面板页脚会出现「管理」按钮跳过去 | +| **跨域数据源地址** | `https://你的隧道地址`(不带路径)—— 面板从这里取数据 | +| **允许跨域读取的页面地址** | `https://你的用户名.github.io` —— 必须是完整来源,含协议 | + +填完立刻生效。回本地仓库提交改动: + +```bash +git add public/monitors.json && git commit -m "chore: 配置跨域数据源" && git push +``` + +> 配置写在 `local.monitors.json`(私有层)的话,Pages 上的面板读不到 —— +> 它只能读仓库里的 `public/monitors.json`。所以上面的两步(管理界面填 → 提交仓库)都要做。 + +### 4. 验证 + +打开 `https://你的用户名.github.io/<仓库名>/`,检查: + +- 页脚数据源显示「**远程探针(跨域)**」—— 说明跨域打通了 +- 表格有数据且状态正常 +- 页脚有「管理」按钮,点击能跳到管理端 +- **页面上没有「刷新」按钮** —— 跨域模式下该接口必然被`refreshLocalOnly` 挡下,隐藏比给一个失败的按钮好 + +--- + +## 四、安全要点 + +| 事项 | 说明 | +| --- | --- | +| **管理端 2FA 必须绑定** | TOTP(Google Authenticator 等)+ 10 枚一次性恢复码 | +| **恢复码离线保存** | 只在绑定时显示一次,用掉一枚少一枚 | +| **跨域白名单不要图省事写 `*`** | 代码里不存在 `*` 分支,写了也不生效。必须写完整来源,如 `https://you.github.io` | +| **管理端永不放行跨域** | `/admin` 与 `/api/admin/*` 不带任何 `Access-Control-*` 头,外部页面既读不到也改不了 | +| **会话** | 30 分钟无操作失效,最长 2 小时;进程重启全部失效 | +| **登录限流** | 同一 IP 连续 5 次失败后指数退避锁定 | +| **API Key** | 放 `local.monitors.json` 或环境变量,**绝不要写进 `public/monitors.json`**(会随Pages 公开) | + +--- + +## 五、备选:不用跨域,改用 Actions 提交快照 + +如果不想暴露探针的 HTTPS 地址,可以让数据「流」到 Pages 而不是「拉」: + +```yaml +# .github/workflows/monitor.yml(示意) +on: + schedule: [{ cron: '*/5 * * * *' }] + workflow_dispatch: +jobs: + probe: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: node prober/node/index.js --once --config=public/monitors.json + - run: | + git config user.name github-actions[bot] + git config user.email 41898282+github-actions[bot]@users.noreply.github.com + git add public/status.json + git diff --staged --quiet || git commit -m "chore: status" && git push +``` + +面板用 `?source=static` 读仓库里的 `status.json`。 + +**取舍**:探测跑在 GitHub Runner 上(不是你的机器),延迟 ≤5 分钟,且**没有通知、维护窗口、事件、2FA 管理端**。适合纯展示需求。 + +--- + +## 六、常见问题 + +**面板一直转圈 / 报错** +浏览器控制台看具体报错: +- `Mixed Content ... was blocked` → 探针地址不是 HTTPS,回第一节 +- `blocked by CORS policy` → 「允许跨域读取的页面地址」没填对,或与实际访问地址不一致(含 `www`、端口、大小写) +- `Failed to fetch` 且数据源显示「浏览器直连」 → 面板拿不到数据,退到了兜底模式 + +**页脚没有「管理」按钮** +「面板上的管理入口链接」为空(默认就是空的,为了不在公网暴露管理面位置)。 + +**跨域下没有刷新按钮** +这是预期行为。跨域模式下 `/api/refresh` 会被服务端的 `refreshLocalOnly` 挡下(对端是代理地址而非私网),前端自动隐藏了这个按钮。 + +**改了配置但面板没变** +管理端写的是 `local.monitors.json`(私有层)。如果那部分设置需要 Pages 上的面板看到,得手动同步到 `public/monitors.json` 并提交。 + +**本机开发时前端跨域被拒** +默认白名单放行 `localhost` / `127.0.0.1`且**忽略端口**,所以 `localhost:5173` 这类能直接用。若你在局域网 IP 上跑前端,需把那个地址显式加进白名单。 \ No newline at end of file diff --git a/docs/screenshot-desktop.png b/docs/screenshot-desktop.png index fa341b1..6db996e 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 8138a33..00d4d47 100644 Binary files a/docs/screenshot-mobile.png and b/docs/screenshot-mobile.png differ diff --git a/docs/screenshot-view-cards.png b/docs/screenshot-view-cards.png index 17ba3bb..9c2617c 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 b968171..23e2fbc 100644 Binary files a/docs/screenshot-view-table.png and b/docs/screenshot-view-table.png differ diff --git a/public/assets/app.js b/public/assets/app.js index dc55f75..3c88835 100644 --- a/public/assets/app.js +++ b/public/assets/app.js @@ -85,6 +85,16 @@ async function mapLimit(items, limit, fn) { function setSource(s) { state.source = s; $('source-badge').textContent = `数据源:${SOURCE_LABEL[s] ?? s}`; + // 跨域模式下「刷新」没有意义:/api/refresh 会被服务端的 refreshLocalOnly 挡住 + // (对端是代理地址而非私网),前端也会退回只读。与其给一个必然失败的按钮,不如隐藏。 + const crossOrigin = s === 'remote'; + const btn = $('refresh-btn'); + if (btn) { + btn.hidden = crossOrigin; + btn.title = crossOrigin ? '' : '立即重新跑一轮探测(需要服务端允许)'; + } + const ctrl = document.querySelector('.controls'); + if (ctrl) ctrl.classList.toggle('no-refresh', crossOrigin); } // 跨域数据源:settings.dataUrl 指向另一台机器上的探针(通常是本机,但页面部署在别处)。 diff --git a/public/assets/style.css b/public/assets/style.css index a5acb39..bd0a696 100644 --- a/public/assets/style.css +++ b/public/assets/style.css @@ -176,6 +176,8 @@ body { .view-btn + .view-btn { border-left: 1px solid var(--border); } .view-btn:hover { color: var(--text); } .view-btn.active { background: var(--accent); color: #fff; } +/* 跨域模式下刷新按钮被隐藏(见 app.js setSource),此时控件区只剩视图切换 */ +.controls.no-refresh { gap: 0; } /* ---------- 分组页签 ---------- */ .group-tabs { @@ -549,6 +551,13 @@ tr[data-status="pending"] .ev-badge { background: var(--pending-bg); color: var( .ev-maint { margin-left: 6px; border-radius: 999px; padding: 0 6px; font-size: 10px; font-weight: 650; background: color-mix(in srgb, var(--accent) 14%, transparent); color: var(--accent); } .ev-name { font-weight: 600; } .ev-detail { color: var(--text-2); } +/* 事件表:单元格一律顶部对齐。clamp2 为了保证等高固定了 height:2.8em, + 若单元格按 middle 对齐,17px 的徽标与 36px 的文本框居中基准不同, + 同一行里「类型」会比「时间/目标/详情」低约 9px(实测截图确认)。 */ +.events-table td { vertical-align: top; } +.events-table td > * { vertical-align: top; } +/* 事件行的行高已由 --row-h 固定,clamp2 可以只做上限不必占满两行 */ +.events-table .clamp2 { height: auto; max-height: 2.8em; } /* 事件表的长文本同样限两行,保证与监控表格一致的等高行 */ .ev-name, .ev-detail { white-space: normal; } .ev-name > .clamp2, .ev-detail > .clamp2 { overflow-wrap: anywhere; } diff --git a/public/status.json b/public/status.json index 5c25649..b54cc0e 100644 --- a/public/status.json +++ b/public/status.json @@ -1,8 +1,8 @@ { - "prober": "shiroko-monitor-node/0.12.1", + "prober": "shiroko-monitor-node/0.12.2", "demo": false, - "generatedAt": "2026-10-14T08:19:10.335Z", - "dataUpdatedAt": "2026-10-14T08:19:01.199Z", + "generatedAt": "2026-10-14T08:47:43.133Z", + "dataUpdatedAt": "2026-10-14T08:47:14.355Z", "updateIntervalSec": 300, "dataFilter": "uptimerobot", "probeScope": "all", @@ -11,12 +11,12 @@ "up": 5, "down": 1, "pending": 0, - "avgLatency": 865, + "avgLatency": 1036, "avgUptime90": 83.3, "memory": { - "rssMB": 73.7, - "heapUsedMB": 13.2, - "heapTotalMB": 14.5 + "rssMB": 84.1, + "heapUsedMB": 14.3, + "heapTotalMB": 15.7 } }, "sources": { @@ -24,10 +24,10 @@ "enabled": true, "ok": true, "error": null, - "lastSyncAt": "2026-10-14T08:19:01.199Z", + "lastSyncAt": "2026-10-14T08:43:33.439Z", "count": 6, "syncing": false, - "lastSyncMs": 1791965938931 + "lastSyncMs": 1791967412139 } }, "notify": { @@ -55,189 +55,199 @@ "monitors": { "ur-792059439": { "status": "up", - "latency": 325, + "latency": 337, "code": null, "detail": "UptimeRobot:运行正常", - "lastCheck": "2026-10-14T08:16:00.000Z", + "lastCheck": "2026-10-14T08:42:00.000Z", "history": [ { - "t": "2026-10-13T09:30:00.000Z", + "t": "2026-10-13T09:20:00.000Z", "ok": true, - "ms": 263 + "ms": 302 }, { - "t": "2026-10-13T10:15:00.000Z", + "t": "2026-10-13T10:00:00.000Z", "ok": true, - "ms": 307 + "ms": 437 }, { - "t": "2026-10-13T10:55:00.000Z", + "t": "2026-10-13T10:40:00.000Z", "ok": true, - "ms": 379 + "ms": 246 }, { - "t": "2026-10-13T11:35:00.000Z", + "t": "2026-10-13T11:20:00.000Z", "ok": true, - "ms": 244 + "ms": 395 }, { - "t": "2026-10-13T12:15:00.000Z", + "t": "2026-10-13T12:00:00.000Z", "ok": true, - "ms": 337 + "ms": 311 }, { - "t": "2026-10-13T12:55:00.000Z", + "t": "2026-10-13T12:40:00.000Z", "ok": true, - "ms": 392 + "ms": 415 }, { - "t": "2026-10-13T13:35:00.000Z", + "t": "2026-10-13T13:20:00.000Z", "ok": true, - "ms": 249 + "ms": 278 }, { - "t": "2026-10-13T14:15:00.000Z", - "ok": true, - "ms": 549 - }, - { - "t": "2026-10-13T14:55:00.000Z", - "ok": true, - "ms": 231 - }, - { - "t": "2026-10-13T15:40:00.000Z", - "ok": true, - "ms": 251 - }, - { - "t": "2026-10-13T16:20:00.000Z", - "ok": true, - "ms": 482 - }, - { - "t": "2026-10-13T17:00:00.000Z", - "ok": true, - "ms": 266 - }, - { - "t": "2026-10-13T17:40:00.000Z", - "ok": true, - "ms": 249 - }, - { - "t": "2026-10-13T18:20:00.000Z", - "ok": true, - "ms": 305 - }, - { - "t": "2026-10-13T19:00:00.000Z", - "ok": true, - "ms": 479 - }, - { - "t": "2026-10-13T19:40:00.000Z", - "ok": true, - "ms": 378 - }, - { - "t": "2026-10-13T20:20:00.000Z", - "ok": true, - "ms": 372 - }, - { - "t": "2026-10-13T21:00:00.000Z", - "ok": true, - "ms": 276 - }, - { - "t": "2026-10-13T21:40:00.000Z", - "ok": true, - "ms": 304 - }, - { - "t": "2026-10-13T22:20:00.000Z", - "ok": true, - "ms": 202 - }, - { - "t": "2026-10-13T23:00:00.000Z", - "ok": true, - "ms": 228 - }, - { - "t": "2026-10-13T23:40:00.000Z", - "ok": true, - "ms": 316 - }, - { - "t": "2026-10-14T00:20:00.000Z", + "t": "2026-10-13T14:00:00.000Z", "ok": true, "ms": 356 }, { - "t": "2026-10-14T01:00:00.000Z", + "t": "2026-10-13T14:40:00.000Z", "ok": true, - "ms": 264 + "ms": 165 }, { - "t": "2026-10-14T01:40:00.000Z", + "t": "2026-10-13T15:20:00.000Z", "ok": true, - "ms": 260 + "ms": 301 }, { - "t": "2026-10-14T02:20:00.000Z", + "t": "2026-10-13T16:00:00.000Z", "ok": true, - "ms": 286 + "ms": 411 }, { - "t": "2026-10-14T03:00:00.000Z", + "t": "2026-10-13T16:40:00.000Z", "ok": true, - "ms": 204 + "ms": 245 }, { - "t": "2026-10-14T03:40:00.000Z", + "t": "2026-10-13T17:20:00.000Z", "ok": true, - "ms": 256 + "ms": 273 }, { - "t": "2026-10-14T04:20:00.000Z", + "t": "2026-10-13T18:00:00.000Z", "ok": true, - "ms": 197 + "ms": 386 }, { - "t": "2026-10-14T05:00:00.000Z", + "t": "2026-10-13T18:40:00.000Z", "ok": true, - "ms": 261 + "ms": 243 }, { - "t": "2026-10-14T05:40:00.000Z", + "t": "2026-10-13T19:20:00.000Z", "ok": true, - "ms": 374 + "ms": 535 }, { - "t": "2026-10-14T06:20:00.000Z", + "t": "2026-10-13T20:00:00.000Z", "ok": true, - "ms": 268 + "ms": 369 }, { - "t": "2026-10-14T07:35:00.000Z", + "t": "2026-10-13T20:40:00.000Z", "ok": true, - "ms": 421 + "ms": 405 }, { - "t": "2026-10-14T08:16:00.000Z", + "t": "2026-10-13T21:20:00.000Z", "ok": true, - "ms": 325 + "ms": 274 + }, + { + "t": "2026-10-13T22:00:00.000Z", + "ok": true, + "ms": 350 + }, + { + "t": "2026-10-13T22:40:00.000Z", + "ok": true, + "ms": 196 + }, + { + "t": "2026-10-13T23:20:00.000Z", + "ok": true, + "ms": 306 + }, + { + "t": "2026-10-14T00:00:00.000Z", + "ok": true, + "ms": 386 + }, + { + "t": "2026-10-14T00:40:00.000Z", + "ok": true, + "ms": 180 + }, + { + "t": "2026-10-14T01:20:00.000Z", + "ok": true, + "ms": 392 + }, + { + "t": "2026-10-14T02:05:00.000Z", + "ok": true, + "ms": 244 + }, + { + "t": "2026-10-14T02:45:00.000Z", + "ok": true, + "ms": 208 + }, + { + "t": "2026-10-14T03:25:00.000Z", + "ok": true, + "ms": 269 + }, + { + "t": "2026-10-14T04:05:00.000Z", + "ok": true, + "ms": 281 + }, + { + "t": "2026-10-14T04:45:00.000Z", + "ok": true, + "ms": 205 + }, + { + "t": "2026-10-14T05:25:00.000Z", + "ok": true, + "ms": 338 + }, + { + "t": "2026-10-14T06:05:00.000Z", + "ok": true, + "ms": 348 + }, + { + "t": "2026-10-14T06:45:00.000Z", + "ok": true, + "ms": 309 + }, + { + "t": "2026-10-14T07:25:00.000Z", + "ok": true, + "ms": 437 + }, + { + "t": "2026-10-14T08:06:00.000Z", + "ok": true, + "ms": 546 + }, + { + "t": "2026-10-14T08:42:00.000Z", + "ok": true, + "ms": 337 } ], "daily": { "2026-10-14": { - "total": 4, - "up": 4, - "latSum": 1903, - "latCount": 4, - "latMax": 546 + "total": 7, + "up": 7, + "latSum": 2445, + "latCount": 7, + "latMax": 394 } }, "meta": { @@ -262,28 +272,23 @@ "90": 0 }, "lat": { - "avg": 476, - "max": 546, + "avg": 349, + "max": 394, "days": [ { "key": "2026-10-14", - "avg": 476 + "avg": 349 } ] } }, "ur-795497022": { "status": "up", - "latency": 719, + "latency": 696, "code": null, "detail": "UptimeRobot:运行正常", - "lastCheck": "2026-10-14T08:16:00.000Z", + "lastCheck": "2026-10-14T08:38:00.000Z", "history": [ - { - "t": "2026-10-13T08:55:00.000Z", - "ok": true, - "ms": 939 - }, { "t": "2026-10-13T09:35:00.000Z", "ok": true, @@ -445,28 +450,28 @@ "ms": 707 }, { - "t": "2026-10-14T07:02:00.000Z", + "t": "2026-10-14T07:33:00.000Z", "ok": true, - "ms": 710 + "ms": 985 }, { - "t": "2026-10-14T07:39:00.000Z", + "t": "2026-10-14T08:05:00.000Z", "ok": true, - "ms": 718 + "ms": 712 }, { - "t": "2026-10-14T08:16:00.000Z", + "t": "2026-10-14T08:38:00.000Z", "ok": true, - "ms": 719 + "ms": 696 } ], "daily": { "2026-10-14": { - "total": 4, - "up": 4, - "latSum": 2884, - "latCount": 4, - "latMax": 745 + "total": 7, + "up": 7, + "latSum": 4953, + "latCount": 7, + "latMax": 721 } }, "meta": { @@ -491,206 +496,206 @@ "90": 5432 }, "lat": { - "avg": 721, - "max": 745, + "avg": 708, + "max": 721, "days": [ { "key": "2026-10-14", - "avg": 721 + "avg": 708 } ] } }, "ur-795556828": { "status": "up", - "latency": 369, + "latency": 398, "code": null, "detail": "UptimeRobot:运行正常", - "lastCheck": "2026-10-14T08:16:00.000Z", + "lastCheck": "2026-10-14T08:42:00.000Z", "history": [ { - "t": "2026-10-13T09:00:00.000Z", + "t": "2026-10-13T09:55:00.000Z", "ok": true, - "ms": 264 + "ms": 455 }, { - "t": "2026-10-13T09:40:00.000Z", + "t": "2026-10-13T10:35:00.000Z", "ok": true, - "ms": 305 + "ms": 372 }, { - "t": "2026-10-13T10:20:00.000Z", + "t": "2026-10-13T11:15:00.000Z", "ok": true, - "ms": 420 + "ms": 240 }, { - "t": "2026-10-13T11:00:00.000Z", + "t": "2026-10-13T11:55:00.000Z", "ok": true, - "ms": 227 + "ms": 261 }, { - "t": "2026-10-13T11:40:00.000Z", + "t": "2026-10-13T12:35:00.000Z", "ok": true, - "ms": 389 + "ms": 335 }, { - "t": "2026-10-13T12:20:00.000Z", - "ok": true, - "ms": 302 - }, - { - "t": "2026-10-13T13:00:00.000Z", - "ok": true, - "ms": 375 - }, - { - "t": "2026-10-13T13:40:00.000Z", - "ok": true, - "ms": 287 - }, - { - "t": "2026-10-13T14:20:00.000Z", - "ok": true, - "ms": 352 - }, - { - "t": "2026-10-13T15:00:00.000Z", - "ok": true, - "ms": 404 - }, - { - "t": "2026-10-13T15:40:00.000Z", - "ok": true, - "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", + "t": "2026-10-13T13:15:00.000Z", "ok": true, "ms": 413 }, { - "t": "2026-10-13T18:20:00.000Z", + "t": "2026-10-13T13:55:00.000Z", "ok": true, - "ms": 386 + "ms": 283 }, { - "t": "2026-10-13T19:00:00.000Z", + "t": "2026-10-13T14:35:00.000Z", "ok": true, - "ms": 302 + "ms": 309 }, { - "t": "2026-10-13T19:40:00.000Z", + "t": "2026-10-13T15:20:00.000Z", "ok": true, - "ms": 376 + "ms": 451 }, { - "t": "2026-10-13T20:20:00.000Z", + "t": "2026-10-13T16:00:00.000Z", "ok": true, - "ms": 266 + "ms": 248 }, { - "t": "2026-10-13T21:00:00.000Z", + "t": "2026-10-13T16:40:00.000Z", "ok": true, - "ms": 475 + "ms": 378 }, { - "t": "2026-10-13T21:40:00.000Z", + "t": "2026-10-13T17:20:00.000Z", "ok": true, - "ms": 354 + "ms": 293 }, { - "t": "2026-10-13T22:20:00.000Z", + "t": "2026-10-13T18:00:00.000Z", "ok": true, - "ms": 239 + "ms": 495 }, { - "t": "2026-10-13T23:00:00.000Z", + "t": "2026-10-13T18:40:00.000Z", + "ok": true, + "ms": 391 + }, + { + "t": "2026-10-13T19:20:00.000Z", + "ok": true, + "ms": 338 + }, + { + "t": "2026-10-13T20:00:00.000Z", + "ok": true, + "ms": 363 + }, + { + "t": "2026-10-13T20:40:00.000Z", + "ok": true, + "ms": 499 + }, + { + "t": "2026-10-13T21:20:00.000Z", + "ok": true, + "ms": 372 + }, + { + "t": "2026-10-13T22:00:00.000Z", + "ok": true, + "ms": 194 + }, + { + "t": "2026-10-13T22:40:00.000Z", "ok": true, "ms": 225 }, { - "t": "2026-10-13T23:40:00.000Z", + "t": "2026-10-13T23:20:00.000Z", "ok": true, - "ms": 243 + "ms": 237 }, { - "t": "2026-10-14T00:20:00.000Z", + "t": "2026-10-14T00:00:00.000Z", + "ok": true, + "ms": 215 + }, + { + "t": "2026-10-14T00:40:00.000Z", + "ok": true, + "ms": 298 + }, + { + "t": "2026-10-14T01:20:00.000Z", + "ok": true, + "ms": 450 + }, + { + "t": "2026-10-14T02:00:00.000Z", "ok": true, "ms": 319 }, { - "t": "2026-10-14T01:00:00.000Z", + "t": "2026-10-14T02:40:00.000Z", "ok": true, - "ms": 396 + "ms": 398 }, { - "t": "2026-10-14T01:45:00.000Z", + "t": "2026-10-14T03:20:00.000Z", "ok": true, - "ms": 475 + "ms": 347 }, { - "t": "2026-10-14T02:25:00.000Z", + "t": "2026-10-14T04:00:00.000Z", "ok": true, - "ms": 128 + "ms": 407 }, { - "t": "2026-10-14T03:05:00.000Z", + "t": "2026-10-14T04:40:00.000Z", "ok": true, - "ms": 351 + "ms": 344 }, { - "t": "2026-10-14T03:45:00.000Z", + "t": "2026-10-14T05:20:00.000Z", "ok": true, - "ms": 429 + "ms": 337 }, { - "t": "2026-10-14T04:25:00.000Z", + "t": "2026-10-14T06:00:00.000Z", "ok": true, - "ms": 303 + "ms": 365 }, { - "t": "2026-10-14T05:05:00.000Z", + "t": "2026-10-14T06:40:00.000Z", "ok": true, - "ms": 276 + "ms": 252 }, { - "t": "2026-10-14T05:45:00.000Z", + "t": "2026-10-14T07:20:00.000Z", "ok": true, - "ms": 555 + "ms": 421 }, { - "t": "2026-10-14T06:25:00.000Z", + "t": "2026-10-14T08:06:00.000Z", "ok": true, - "ms": 278 + "ms": 387 }, { - "t": "2026-10-14T07:36:00.000Z", + "t": "2026-10-14T08:42:00.000Z", "ok": true, - "ms": 204 - }, - { - "t": "2026-10-14T08:16:00.000Z", - "ok": true, - "ms": 369 + "ms": 398 } ], "daily": { "2026-10-14": { - "total": 4, - "up": 4, - "latSum": 1530, - "latCount": 4, - "latMax": 387 + "total": 7, + "up": 7, + "latSum": 2618, + "latCount": 7, + "latMax": 421 } }, "meta": { @@ -715,28 +720,23 @@ "90": 0 }, "lat": { - "avg": 383, - "max": 387, + "avg": 374, + "max": 421, "days": [ { "key": "2026-10-14", - "avg": 383 + "avg": 374 } ] } }, "ur-792059414": { "status": "up", - "latency": 2428, + "latency": 3208, "code": null, "detail": "UptimeRobot:运行正常", - "lastCheck": "2026-10-14T08:17:00.000Z", + "lastCheck": "2026-10-14T08:40:00.000Z", "history": [ - { - "t": "2026-10-13T09:10:00.000Z", - "ok": true, - "ms": 1991 - }, { "t": "2026-10-13T09:55:00.000Z", "ok": true, @@ -783,123 +783,133 @@ "ms": 2097 }, { - "t": "2026-10-13T16:35:00.000Z", + "t": "2026-10-13T16:30:00.000Z", "ok": true, - "ms": 2206 + "ms": 2060 }, { - "t": "2026-10-13T17:20:00.000Z", + "t": "2026-10-13T17:15:00.000Z", "ok": true, - "ms": 2293 + "ms": 2264 }, { - "t": "2026-10-13T18:05:00.000Z", + "t": "2026-10-13T17:55:00.000Z", "ok": true, - "ms": 1347 + "ms": 1246 }, { - "t": "2026-10-13T18:50:00.000Z", + "t": "2026-10-13T18:35:00.000Z", "ok": true, - "ms": 1656 + "ms": 1451 }, { - "t": "2026-10-13T19:35:00.000Z", + "t": "2026-10-13T19:15:00.000Z", "ok": true, - "ms": 2883 + "ms": 3225 }, { - "t": "2026-10-13T20:20:00.000Z", + "t": "2026-10-13T19:55:00.000Z", "ok": true, - "ms": 2649 + "ms": 2138 }, { - "t": "2026-10-13T21:05:00.000Z", + "t": "2026-10-13T20:35:00.000Z", "ok": true, - "ms": 2923 + "ms": 2633 }, { - "t": "2026-10-13T21:50:00.000Z", + "t": "2026-10-13T21:15:00.000Z", "ok": true, - "ms": 1964 + "ms": 3605 }, { - "t": "2026-10-13T22:35:00.000Z", + "t": "2026-10-13T21:55:00.000Z", "ok": true, - "ms": 2335 + "ms": 1785 }, { - "t": "2026-10-13T23:20:00.000Z", + "t": "2026-10-13T22:40:00.000Z", "ok": true, - "ms": 2418 + "ms": 1904 }, { - "t": "2026-10-14T00:05:00.000Z", + "t": "2026-10-13T23:25:00.000Z", "ok": true, - "ms": 2098 + "ms": 2527 }, { - "t": "2026-10-14T00:50:00.000Z", + "t": "2026-10-14T00:10:00.000Z", "ok": true, - "ms": 1921 + "ms": 2372 }, { - "t": "2026-10-14T01:35:00.000Z", + "t": "2026-10-14T00:55:00.000Z", "ok": true, - "ms": 3110 + "ms": 1934 }, { - "t": "2026-10-14T02:20:00.000Z", + "t": "2026-10-14T01:40:00.000Z", "ok": true, - "ms": 1719 + "ms": 3003 }, { - "t": "2026-10-14T03:05:00.000Z", + "t": "2026-10-14T02:25:00.000Z", "ok": true, - "ms": 1623 + "ms": 1655 }, { - "t": "2026-10-14T03:45:00.000Z", + "t": "2026-10-14T03:10:00.000Z", "ok": true, - "ms": 3219 + "ms": 1873 }, { - "t": "2026-10-14T04:25:00.000Z", + "t": "2026-10-14T03:55:00.000Z", "ok": true, - "ms": 2408 + "ms": 3017 }, { - "t": "2026-10-14T05:05:00.000Z", + "t": "2026-10-14T04:40:00.000Z", "ok": true, - "ms": 2619 + "ms": 1955 }, { - "t": "2026-10-14T05:45:00.000Z", + "t": "2026-10-14T05:20:00.000Z", "ok": true, - "ms": 2976 + "ms": 3605 }, { - "t": "2026-10-14T06:25:00.000Z", + "t": "2026-10-14T06:00:00.000Z", "ok": true, - "ms": 1855 + "ms": 2050 }, { - "t": "2026-10-14T07:38:00.000Z", + "t": "2026-10-14T06:40:00.000Z", "ok": true, - "ms": 2010 + "ms": 1494 }, { - "t": "2026-10-14T08:17:00.000Z", + "t": "2026-10-14T07:20:00.000Z", "ok": true, - "ms": 2428 + "ms": 2086 + }, + { + "t": "2026-10-14T08:00:00.000Z", + "ok": true, + "ms": 1610 + }, + { + "t": "2026-10-14T08:40:00.000Z", + "ok": true, + "ms": 3208 } ], "daily": { "2026-10-14": { - "total": 4, - "up": 4, - "latSum": 8517, - "latCount": 4, - "latMax": 2428 + "total": 7, + "up": 7, + "latSum": 20805, + "latCount": 7, + "latMax": 3208 } }, "meta": { @@ -924,201 +934,206 @@ "90": 7593 }, "lat": { - "avg": 2129, - "max": 2428, + "avg": 2972, + "max": 3208, "days": [ { "key": "2026-10-14", - "avg": 2129 + "avg": 2972 } ] } }, "ur-798788113": { "status": "up", - "latency": 486, + "latency": 541, "code": null, "detail": "UptimeRobot:运行正常", - "lastCheck": "2026-10-14T08:17:00.000Z", + "lastCheck": "2026-10-14T08:43:00.000Z", "history": [ { - "t": "2026-10-13T09:30:00.000Z", - "ok": true, - "ms": 545 - }, - { - "t": "2026-10-13T10:10:00.000Z", - "ok": true, - "ms": 456 - }, - { - "t": "2026-10-13T10:50:00.000Z", - "ok": true, - "ms": 394 - }, - { - "t": "2026-10-13T11:30:00.000Z", - "ok": true, - "ms": 398 - }, - { - "t": "2026-10-13T12:10:00.000Z", - "ok": true, - "ms": 588 - }, - { - "t": "2026-10-13T12:50:00.000Z", - "ok": true, - "ms": 430 - }, - { - "t": "2026-10-13T13:30:00.000Z", - "ok": true, - "ms": 502 - }, - { - "t": "2026-10-13T14:15:00.000Z", - "ok": true, - "ms": 355 - }, - { - "t": "2026-10-13T14:55:00.000Z", - "ok": true, - "ms": 358 - }, - { - "t": "2026-10-13T15:35:00.000Z", - "ok": true, - "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", + "t": "2026-10-13T09:55:00.000Z", "ok": true, "ms": 602 }, { - "t": "2026-10-13T18:55:00.000Z", + "t": "2026-10-13T10:35:00.000Z", "ok": true, - "ms": 363 + "ms": 519 }, { - "t": "2026-10-13T19:40:00.000Z", + "t": "2026-10-13T11:15:00.000Z", "ok": true, - "ms": 1380 + "ms": 325 }, { - "t": "2026-10-13T20:20:00.000Z", + "t": "2026-10-13T11:55:00.000Z", "ok": true, - "ms": 512 + "ms": 933 }, { - "t": "2026-10-13T21:00:00.000Z", + "t": "2026-10-13T12:35:00.000Z", "ok": true, - "ms": 394 + "ms": 424 }, { - "t": "2026-10-13T21:40:00.000Z", + "t": "2026-10-13T13:15:00.000Z", "ok": true, - "ms": 238 + "ms": 460 }, { - "t": "2026-10-13T22:20:00.000Z", + "t": "2026-10-13T13:55:00.000Z", "ok": true, - "ms": 323 + "ms": 496 }, { - "t": "2026-10-13T23:00:00.000Z", + "t": "2026-10-13T14:35:00.000Z", "ok": true, - "ms": 353 + "ms": 438 }, { - "t": "2026-10-13T23:40:00.000Z", + "t": "2026-10-13T15:15:00.000Z", "ok": true, - "ms": 179 + "ms": 459 }, { - "t": "2026-10-14T00:20:00.000Z", + "t": "2026-10-13T15:55:00.000Z", "ok": true, - "ms": 263 + "ms": 349 }, { - "t": "2026-10-14T01:00:00.000Z", + "t": "2026-10-13T16:35:00.000Z", "ok": true, - "ms": 381 + "ms": 367 }, { - "t": "2026-10-14T01:40:00.000Z", + "t": "2026-10-13T17:15:00.000Z", "ok": true, - "ms": 378 + "ms": 504 }, { - "t": "2026-10-14T02:20:00.000Z", + "t": "2026-10-13T17:55:00.000Z", "ok": true, - "ms": 278 + "ms": 286 }, { - "t": "2026-10-14T03:00:00.000Z", + "t": "2026-10-13T18:35:00.000Z", "ok": true, - "ms": 280 + "ms": 389 }, { - "t": "2026-10-14T03:40:00.000Z", + "t": "2026-10-13T19:15:00.000Z", "ok": true, - "ms": 322 + "ms": 1491 }, { - "t": "2026-10-14T04:20:00.000Z", + "t": "2026-10-13T19:55:00.000Z", "ok": true, - "ms": 323 + "ms": 456 }, { - "t": "2026-10-14T05:00:00.000Z", + "t": "2026-10-13T20:35:00.000Z", "ok": true, - "ms": 321 + "ms": 433 }, { - "t": "2026-10-14T05:40:00.000Z", + "t": "2026-10-13T21:15:00.000Z", "ok": true, - "ms": 642 + "ms": 426 }, { - "t": "2026-10-14T06:20:00.000Z", + "t": "2026-10-13T21:55:00.000Z", "ok": true, - "ms": 315 + "ms": 249 }, { - "t": "2026-10-14T07:36:00.000Z", + "t": "2026-10-13T22:35:00.000Z", "ok": true, - "ms": 363 + "ms": 359 }, { - "t": "2026-10-14T08:17:00.000Z", + "t": "2026-10-13T23:15:00.000Z", "ok": true, - "ms": 486 + "ms": 311 + }, + { + "t": "2026-10-13T23:55:00.000Z", + "ok": true, + "ms": 222 + }, + { + "t": "2026-10-14T00:35:00.000Z", + "ok": true, + "ms": 332 + }, + { + "t": "2026-10-14T01:15:00.000Z", + "ok": true, + "ms": 328 + }, + { + "t": "2026-10-14T01:55:00.000Z", + "ok": true, + "ms": 300 + }, + { + "t": "2026-10-14T02:35:00.000Z", + "ok": true, + "ms": 405 + }, + { + "t": "2026-10-14T03:15:00.000Z", + "ok": true, + "ms": 258 + }, + { + "t": "2026-10-14T03:55:00.000Z", + "ok": true, + "ms": 274 + }, + { + "t": "2026-10-14T04:35:00.000Z", + "ok": true, + "ms": 265 + }, + { + "t": "2026-10-14T05:15:00.000Z", + "ok": true, + "ms": 373 + }, + { + "t": "2026-10-14T06:00:00.000Z", + "ok": true, + "ms": 675 + }, + { + "t": "2026-10-14T06:40:00.000Z", + "ok": true, + "ms": 525 + }, + { + "t": "2026-10-14T07:20:00.000Z", + "ok": true, + "ms": 435 + }, + { + "t": "2026-10-14T08:07:00.000Z", + "ok": true, + "ms": 444 + }, + { + "t": "2026-10-14T08:43:00.000Z", + "ok": true, + "ms": 541 } ], "daily": { "2026-10-14": { - "total": 4, - "up": 4, - "latSum": 1756, - "latCount": 4, - "latMax": 486 + "total": 7, + "up": 7, + "latSum": 3014, + "latCount": 7, + "latMax": 541 } }, "meta": { @@ -1143,12 +1158,12 @@ "90": 0 }, "lat": { - "avg": 439, - "max": 486, + "avg": 431, + "max": 541, "days": [ { "key": "2026-10-14", - "avg": 439 + "avg": 431 } ] } @@ -1168,7 +1183,7 @@ ], "daily": { "2026-10-14": { - "total": 4, + "total": 7, "up": 0 } }, diff --git a/scripts/verify-views.cjs b/scripts/verify-views.cjs index 0c0b029..dbec830 100644 --- a/scripts/verify-views.cjs +++ b/scripts/verify-views.cjs @@ -413,6 +413,29 @@ async function main() { return { down: down ? getComputedStyle(down).backgroundColor : '无', up: up ? getComputedStyle(up).backgroundColor : '无', downCells: down ? down.querySelectorAll('td').length : 0 }; }); check('事件行复用表格状态底色(故障行着色、恢复行不染)', /^(color\(|rgba?\(\d)/.test(evBg.down) && evBg.down !== 'rgba(0, 0, 0, 0)' && evBg.up === 'rgba(0, 0, 0, 0)' && evBg.downCells === 4, `故障=${evBg.down.slice(0, 30)} 恢复=${evBg.up} 列数=${evBg.downCells}`); + // 事件行的四列文字必须落在同一水平线上。此前 clamp2 固定 height:2.8em(36px)、 + // 而徽标只有 17px,单元格按 middle 对齐时两者居中基准不同, + // 「类型」会比「时间/目标/详情」低约 9px(实测截图确认)。 + const evAlign = await dp.evaluate(() => { + const rows = [...document.querySelectorAll('.events-table tbody tr')]; + const spreads = []; + for (const tr of rows) { + const tops = []; + for (const td of tr.querySelectorAll('td')) { + // Range 要用独立实例:selectNodeContents 返回的是布尔值,不是 Range + const range = document.createRange(); + range.selectNodeContents(td); + const rects = [...range.getClientRects()]; + if (rects.length) tops.push(Math.round(rects[0].top)); + } + if (tops.length) spreads.push(Math.max(...tops) - Math.min(...tops)); + } + return { worst: spreads.length ? Math.max(...spreads) : -1, rows: spreads.length }; + }); + check('事件行各列文字顶端对齐(徽标与文本同一水平线)', evAlign.worst === 0, `最大错位 ${evAlign.worst}px(${evAlign.rows} 行)`); + // 跨域模式下隐藏刷新按钮(该模式下 /api/refresh 必然被 refreshLocalOnly 挡下) + const refreshBtnShown = await dp.evaluate(() => !document.getElementById('refresh-btn').hidden); + check('同源模式下保留刷新按钮', refreshBtnShown === true, `可见=${refreshBtnShown}`); 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);