diff --git a/README.md b/README.md index 803c23c..f494a20 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,8 @@ shiroko-monitor/ │ ├── verify-views.cjs # 视图渲染、切换持久化与数据源过滤 │ ├── verify-security.cjs # banner 探测 / 脱敏 / 私有分层 / 刷新限流 │ ├── verify-uptimerobot.cjs # UR 映射单测 / 兼容接口 / 失败路径 / 真实 Key 冒烟 -│ └── prepare-artwork.py # 从 assets-src/ 原图生成透明素材(边界 + 封闭区域两步抠图) +│ ├── prepare-artwork.py # 从 assets-src/ 原图生成透明素材(边界 + 封闭区域两步抠图) +│ └── verify-layout.cjs # 布局回归:极端内容 × 13 视口 × 2 视图,断言零横向滚动 ├── assets-src/ # 插画原始素材(AI 生成的白底原图,供重新处理) ├── local.monitors.json # 私有配置(不入库):真实地址、API Key ├── edge/cloudflare/ # Cloudflare Workers 边缘探针 @@ -444,6 +445,15 @@ UptimeRobot 的探测来自其多个节点,适合作为**异地视角**与自 - [x] 页脚改为展示「数据更新于 … + 下次检查 Ns」,tooltip 内含服务端更新节奏与快照生成时间 - [x] `scripts/verify.cjs` 适配纯外部数据源部署(自动跳过不适用的探测类断言),并新增「倒计时对齐服务端」断言 +### v0.9.1 已实现 + +- [x] **「任何情况下都不需要横向滚动」定为硬要求并落地**:表格改为**流式列宽**(`table-layout: fixed` + 百分比列宽),表头允许换行、单元格裁剪,心率图与可用率条随宽度伸缩;从结构上不可能撑出横向滚动 +- [x] 修复两个只有极端内容才暴露的撑破(普通数据下测不出来): + - **卡片视图**在 <768px 曾溢出最多 495px —— grid 轨道 `minmax(300px, 1fr)` + 网格项默认 `min-width: auto`,被超长 URL 撑到 801px;改为 `minmax(min(300px, 100%), 1fr)` + `.card { min-width: 0 }` + - **表格**在 <768px 曾溢出 8~42px —— 表头 `white-space: nowrap` 在窄列里溢出;改为换行 + 裁剪 +- [x] 新增 `scripts/verify-layout.cjs`:自带极端配置(40 目标 / 6 分组 / 超长名称与 URL)+ 临时静态服务,扫描 **13 种视口 × 2 视图**,26 项断言零横向滚动 +- [x] 被截断的名称/目标补 `title`,鼠标悬停可看完整值 + ### v0.9.0 已实现 **修复你反馈的两个问题** diff --git a/docs/screenshot-desktop.png b/docs/screenshot-desktop.png index 4a8c15c..1f03426 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 7ef886a..83b4635 100644 Binary files a/docs/screenshot-mobile.png and b/docs/screenshot-mobile.png differ diff --git a/docs/screenshot-narrow-table.png b/docs/screenshot-narrow-table.png new file mode 100644 index 0000000..338721e Binary files /dev/null and b/docs/screenshot-narrow-table.png differ diff --git a/docs/screenshot-view-cards.png b/docs/screenshot-view-cards.png index 8187c6a..947fe19 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 9439217..be1ed56 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 742de04..10d15e3 100644 --- a/edge/cloudflare/worker.js +++ b/edge/cloudflare/worker.js @@ -11,7 +11,7 @@ import { connect } from 'cloudflare:sockets'; -const VERSION = '0.9.0'; +const VERSION = '0.9.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 ea8a248..488cee5 100644 --- a/prober/node/index.js +++ b/prober/node/index.js @@ -35,7 +35,7 @@ const CONCURRENCY = 8; const WRITE_DEBOUNCE_MS = 300; const DEFAULT_INTERVAL_SEC = 60; const PORT = Number(process.env.PORT || 8787); -const VERSION = '0.9.0'; +const VERSION = '0.9.1'; const UR_DEFAULT_POLL_SEC = 300; const UR_MIN_REFRESH_GAP_MS = 60000; diff --git a/public/assets/app.js b/public/assets/app.js index f4f76b8..84e8336 100644 --- a/public/assets/app.js +++ b/public/assets/app.js @@ -1,4 +1,4 @@ -const VERSION = '0.9.0'; +const VERSION = '0.9.1'; const HB_LIMIT = 48; const UPTIME_DAYS = 90; const BROWSER_CONCURRENCY = 6; @@ -285,7 +285,8 @@ function renderCard(m) { const head = document.createElement('div'); head.className = 'card-head'; - head.innerHTML = `

${escapeHtml(m.name)}

${escapeHtml(typeLabel(m))}`; + // 名称列在窄屏会被截断,title 保证鼠标悬停仍能看到完整名称 + head.innerHTML = `

${escapeHtml(m.name)}

${escapeHtml(typeLabel(m))}`; const target = document.createElement('div'); target.className = 'card-target'; @@ -466,10 +467,9 @@ function tableRow(m, hide) { const tr = document.createElement('tr'); tr.dataset.status = s.cls; tr.dataset.id = m.id; - const targetCell = hide ? '' : `${escapeHtml(targetText(m))}`; - tr.innerHTML = + const targetCell = hide ? '' : `${escapeHtml(targetText(m))}`; tr.innerHTML = `${s.label}` + - `${escapeHtml(m.name)}${escapeHtml(typeLabel(m))}` + + `${escapeHtml(m.name)}${escapeHtml(typeLabel(m))}` + targetCell + `${st?.latency != null ? `${st.latency} ms` : '–'}` + `${escapeHtml(resultText(st, m))}` + diff --git a/public/assets/style.css b/public/assets/style.css index cc4a023..1208b76 100644 --- a/public/assets/style.css +++ b/public/assets/style.css @@ -199,7 +199,7 @@ body { /* ---------- 卡片网格 ---------- */ .grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + grid-template-columns: repeat(auto-fill, minmax(min(300px, 100%), 1fr)); gap: 14px; max-width: 1200px; margin: 0 auto; @@ -218,6 +218,9 @@ body { display: flex; flex-direction: column; gap: 10px; + /* 网格项默认 min-width:auto,长 URL/名称会把卡片撑破容器 —— 显式允许收缩并裁剪 */ + min-width: 0; + overflow: hidden; padding: 14px 16px; background: var(--surface); border: 1px solid var(--border); @@ -358,18 +361,35 @@ body[data-view="table"] .grid { display: block; } box-shadow: var(--shadow); } -.monitor-table { width: 100%; border-collapse: collapse; font-size: 13px; } +.monitor-table { + width: 100%; + /* 列宽按百分比分配 + 固定表格算法:表格永远恰好铺满容器宽度,不会撑出横向滚动 */ + table-layout: fixed; + border-collapse: collapse; + font-size: 13px; +} +/* 列宽:状态 / 服务 / 目标 / 延迟 / 结果 / 最近检测 / 心率 / 90天可用率 = 100% */ +.monitor-table th:nth-child(1) { width: 8%; } +.monitor-table th:nth-child(2) { width: 16%; } +.monitor-table th:nth-child(3) { width: 21%; } +.monitor-table th:nth-child(4) { width: 8%; } +.monitor-table th:nth-child(5) { width: 7%; } +.monitor-table th:nth-child(6) { width: 11%; } +.monitor-table th:nth-child(7) { width: 17%; } +.monitor-table th:nth-child(8) { width: 12%; } .monitor-table th { padding: 10px 12px; text-align: left; font-size: 12px; font-weight: 600; color: var(--text-2); - white-space: nowrap; + white-space: normal; + overflow: hidden; + text-overflow: ellipsis; background: var(--bg); border-bottom: 1px solid var(--border); } -.monitor-table td { padding: 9px 12px; border-bottom: 1px solid var(--border); vertical-align: middle; } +.monitor-table td { padding: 9px 12px; border-bottom: 1px solid var(--border); vertical-align: middle; overflow: hidden; } .monitor-table tbody tr:last-child td { border-bottom: 0; } .monitor-table tbody tr:hover { background: color-mix(in srgb, var(--accent) 5%, transparent); } .monitor-table tbody tr[data-status="down"] { background: color-mix(in srgb, var(--down) 6%, transparent); } @@ -380,7 +400,7 @@ body[data-view="table"] .grid { display: block; } tr[data-status="up"] .t-state { color: var(--up-text); } tr[data-status="down"] .t-state { color: var(--down-text); } tr[data-status="pending"] .t-state { color: var(--pending-text); } -.t-name { white-space: nowrap; } +.t-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .t-name .type-badge { margin-left: 8px; } .t-target { max-width: 280px; @@ -392,9 +412,12 @@ tr[data-status="pending"] .t-state { color: var(--pending-text); } white-space: nowrap; } .t-num { font-variant-numeric: tabular-nums; white-space: nowrap; } -.t-hb { width: 160px; } -.t-hb svg { display: block; width: 150px; height: 24px; } +.t-hb svg { display: block; width: 100%; max-width: 150px; height: 24px; } .t-uptime { white-space: nowrap; } -.uptime-bar-mini { display: inline-flex; width: 110px; height: 8px; margin-right: 8px; vertical-align: middle; } +/* 可用率条随视口伸缩;极窄时只留百分比,避免挤压其他列 */ +.uptime-bar-mini { display: inline-flex; width: clamp(40px, 8vw, 110px); height: 8px; margin-right: 8px; vertical-align: middle; } .t-pct { font-size: 12px; color: var(--text-2); font-variant-numeric: tabular-nums; } +@media (max-width: 640px) { + .uptime-bar-mini { display: none; } +} diff --git a/public/index.html b/public/index.html index b210701..a165c99 100644 --- a/public/index.html +++ b/public/index.html @@ -56,7 +56,7 @@ – – - shiroko-monitor v0.9.0 + shiroko-monitor v0.9.1 diff --git a/public/status.json b/public/status.json index c088903..1a77fbb 100644 --- a/public/status.json +++ b/public/status.json @@ -1,8 +1,8 @@ { "prober": "shiroko-monitor-node/0.7.0", "demo": false, - "generatedAt": "2026-10-13T13:55:30.243Z", - "dataUpdatedAt": "2026-10-13T13:54:57.645Z", + "generatedAt": "2026-10-13T14:36:59.637Z", + "dataUpdatedAt": "2026-10-13T14:36:43.139Z", "updateIntervalSec": 300, "dataFilter": "uptimerobot", "stats": { @@ -10,12 +10,12 @@ "up": 5, "down": 1, "pending": 0, - "avgLatency": 1110, + "avgLatency": 874, "avgUptime90": 83.3, "memory": { - "rssMB": 78.9, - "heapUsedMB": 10.2, - "heapTotalMB": 16 + "rssMB": 81.3, + "heapUsedMB": 13.8, + "heapTotalMB": 15.7 } }, "sources": { @@ -23,200 +23,200 @@ "enabled": true, "ok": true, "error": null, - "lastSyncAt": "2026-10-13T13:52:32.984Z", + "lastSyncAt": "2026-10-13T14:36:16.338Z", "count": 6, "syncing": false, - "lastSyncMs": 1791899551553 + "lastSyncMs": 1791902174992 } }, "monitors": { "ur-792059439": { "status": "up", - "latency": 247, + "latency": 464, "code": null, "detail": "UptimeRobot:运行正常", - "lastCheck": "2026-10-13T13:48:00.000Z", + "lastCheck": "2026-10-13T14:34:00.000Z", "history": [ { - "t": "2026-10-12T15:00:00.000Z", + "t": "2026-10-12T15:45:00.000Z", "ok": true, - "ms": 312 + "ms": 392 }, { - "t": "2026-10-12T15:40:00.000Z", + "t": "2026-10-12T16:25:00.000Z", "ok": true, - "ms": 336 + "ms": 288 }, { - "t": "2026-10-12T16:20:00.000Z", + "t": "2026-10-12T17:05:00.000Z", "ok": true, - "ms": 275 + "ms": 273 }, { - "t": "2026-10-12T17:00:00.000Z", + "t": "2026-10-12T17:45:00.000Z", "ok": true, - "ms": 255 + "ms": 391 }, { - "t": "2026-10-12T17:40:00.000Z", + "t": "2026-10-12T18:25:00.000Z", "ok": true, - "ms": 304 + "ms": 326 }, { - "t": "2026-10-12T18:20:00.000Z", + "t": "2026-10-12T19:05:00.000Z", "ok": true, - "ms": 323 + "ms": 333 }, { - "t": "2026-10-12T19:00:00.000Z", + "t": "2026-10-12T19:45:00.000Z", "ok": true, - "ms": 328 + "ms": 249 }, { - "t": "2026-10-12T19:40:00.000Z", + "t": "2026-10-12T20:25:00.000Z", "ok": true, - "ms": 304 + "ms": 262 }, { - "t": "2026-10-12T20:20:00.000Z", + "t": "2026-10-12T21:05:00.000Z", "ok": true, - "ms": 265 + "ms": 290 }, { - "t": "2026-10-12T21:00:00.000Z", + "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", "ok": true, "ms": 294 }, { - "t": "2026-10-12T21:40:00.000Z", + "t": "2026-10-13T06:35:00.000Z", "ok": true, - "ms": 379 + "ms": 238 }, { - "t": "2026-10-12T22:20:00.000Z", + "t": "2026-10-13T07:15:00.000Z", "ok": true, - "ms": 176 + "ms": 259 }, { - "t": "2026-10-12T23:05:00.000Z", + "t": "2026-10-13T07:55:00.000Z", "ok": true, - "ms": 286 + "ms": 227 }, { - "t": "2026-10-12T23:45:00.000Z", + "t": "2026-10-13T08:35:00.000Z", "ok": true, - "ms": 219 + "ms": 434 }, { - "t": "2026-10-13T00:25:00.000Z", + "t": "2026-10-13T09:15:00.000Z", "ok": true, - "ms": 207 + "ms": 333 }, { - "t": "2026-10-13T01:05:00.000Z", + "t": "2026-10-13T09:55:00.000Z", "ok": true, - "ms": 162 + "ms": 427 }, { - "t": "2026-10-13T01:45:00.000Z", + "t": "2026-10-13T10:35:00.000Z", "ok": true, - "ms": 160 + "ms": 233 }, { - "t": "2026-10-13T02:25:00.000Z", + "t": "2026-10-13T11:15:00.000Z", "ok": true, - "ms": 199 + "ms": 388 }, { - "t": "2026-10-13T03:05:00.000Z", + "t": "2026-10-13T11:55:00.000Z", "ok": true, - "ms": 188 + "ms": 329 }, { - "t": "2026-10-13T03:45:00.000Z", + "t": "2026-10-13T12:35:00.000Z", "ok": true, - "ms": 130 + "ms": 422 }, { - "t": "2026-10-13T04:25:00.000Z", + "t": "2026-10-13T13:15:00.000Z", "ok": true, - "ms": 494 + "ms": 271 }, { - "t": "2026-10-13T05:05:00.000Z", + "t": "2026-10-13T13:53:00.000Z", "ok": true, - "ms": 310 + "ms": 294 }, { - "t": "2026-10-13T05:45:00.000Z", + "t": "2026-10-13T14:34:00.000Z", "ok": true, - "ms": 354 - }, - { - "t": "2026-10-13T06:25:00.000Z", - "ok": true, - "ms": 275 - }, - { - "t": "2026-10-13T07:05:00.000Z", - "ok": true, - "ms": 231 - }, - { - "t": "2026-10-13T07:45:00.000Z", - "ok": true, - "ms": 237 - }, - { - "t": "2026-10-13T08:25:00.000Z", - "ok": true, - "ms": 467 - }, - { - "t": "2026-10-13T09:05:00.000Z", - "ok": true, - "ms": 409 - }, - { - "t": "2026-10-13T09:45:00.000Z", - "ok": true, - "ms": 424 - }, - { - "t": "2026-10-13T10:25:00.000Z", - "ok": true, - "ms": 171 - }, - { - "t": "2026-10-13T11:05:00.000Z", - "ok": true, - "ms": 377 - }, - { - "t": "2026-10-13T11:45:00.000Z", - "ok": true, - "ms": 304 - }, - { - "t": "2026-10-13T12:25:00.000Z", - "ok": true, - "ms": 426 - }, - { - "t": "2026-10-13T13:08:00.000Z", - "ok": true, - "ms": 250 - }, - { - "t": "2026-10-13T13:48:00.000Z", - "ok": true, - "ms": 247 + "ms": 464 } ], "daily": { "2026-10-13": { - "total": 35, - "up": 35 + "total": 46, + "up": 46 } }, "meta": { @@ -243,191 +243,186 @@ }, "ur-795497022": { "status": "up", - "latency": 1131, + "latency": 704, "code": null, "detail": "UptimeRobot:运行正常", - "lastCheck": "2026-10-13T13:50:00.000Z", + "lastCheck": "2026-10-13T14:33:00.000Z", "history": [ { - "t": "2026-10-12T14:35:00.000Z", + "t": "2026-10-12T15:40:00.000Z", "ok": true, - "ms": 814 + "ms": 798 }, { - "t": "2026-10-12T15:15:00.000Z", - "ok": true, - "ms": 710 - }, - { - "t": "2026-10-12T15:55:00.000Z", - "ok": true, - "ms": 792 - }, - { - "t": "2026-10-12T16:35:00.000Z", - "ok": true, - "ms": 700 - }, - { - "t": "2026-10-12T17:15:00.000Z", - "ok": true, - "ms": 689 - }, - { - "t": "2026-10-12T17:55:00.000Z", - "ok": true, - "ms": 725 - }, - { - "t": "2026-10-12T18:35:00.000Z", - "ok": true, - "ms": 744 - }, - { - "t": "2026-10-12T19:15:00.000Z", - "ok": true, - "ms": 723 - }, - { - "t": "2026-10-12T19:55:00.000Z", - "ok": true, - "ms": 698 - }, - { - "t": "2026-10-12T20:35:00.000Z", - "ok": true, - "ms": 768 - }, - { - "t": "2026-10-12T21:15:00.000Z", - "ok": true, - "ms": 616 - }, - { - "t": "2026-10-12T21:55:00.000Z", - "ok": true, - "ms": 626 - }, - { - "t": "2026-10-12T22:35:00.000Z", - "ok": true, - "ms": 674 - }, - { - "t": "2026-10-12T23:15:00.000Z", - "ok": true, - "ms": 624 - }, - { - "t": "2026-10-12T23:55:00.000Z", - "ok": true, - "ms": 640 - }, - { - "t": "2026-10-13T00:35:00.000Z", - "ok": true, - "ms": 673 - }, - { - "t": "2026-10-13T01:15:00.000Z", - "ok": true, - "ms": 609 - }, - { - "t": "2026-10-13T01:55:00.000Z", - "ok": true, - "ms": 759 - }, - { - "t": "2026-10-13T02:35:00.000Z", - "ok": true, - "ms": 581 - }, - { - "t": "2026-10-13T03:15:00.000Z", - "ok": true, - "ms": 666 - }, - { - "t": "2026-10-13T03:55:00.000Z", - "ok": true, - "ms": 662 - }, - { - "t": "2026-10-13T04:35:00.000Z", + "t": "2026-10-12T16:20:00.000Z", "ok": true, "ms": 685 }, { - "t": "2026-10-13T05:15:00.000Z", + "t": "2026-10-12T17:00:00.000Z", "ok": true, - "ms": 647 + "ms": 700 }, { - "t": "2026-10-13T05:55:00.000Z", + "t": "2026-10-12T17:40:00.000Z", "ok": true, - "ms": 588 + "ms": 694 }, { - "t": "2026-10-13T06:40:00.000Z", + "t": "2026-10-12T18:20:00.000Z", "ok": true, - "ms": 625 + "ms": 703 }, { - "t": "2026-10-13T07:20:00.000Z", + "t": "2026-10-12T19:00:00.000Z", "ok": true, - "ms": 673 + "ms": 729 }, { - "t": "2026-10-13T08:00:00.000Z", + "t": "2026-10-12T19:40:00.000Z", "ok": true, - "ms": 758 + "ms": 713 }, { - "t": "2026-10-13T08:40:00.000Z", + "t": "2026-10-12T20:20:00.000Z", "ok": true, - "ms": 826 + "ms": 784 }, { - "t": "2026-10-13T09:20:00.000Z", + "t": "2026-10-12T21:00:00.000Z", "ok": true, - "ms": 749 + "ms": 628 }, { - "t": "2026-10-13T10:00:00.000Z", + "t": "2026-10-12T21:40:00.000Z", "ok": true, - "ms": 791 + "ms": 629 }, { - "t": "2026-10-13T10:40:00.000Z", + "t": "2026-10-12T22:20:00.000Z", "ok": true, - "ms": 678 + "ms": 751 }, { - "t": "2026-10-13T11:20:00.000Z", + "t": "2026-10-12T23:00:00.000Z", "ok": true, - "ms": 740 + "ms": 624 }, { - "t": "2026-10-13T12:40:00.000Z", + "t": "2026-10-12T23:40:00.000Z", "ok": true, - "ms": 771 + "ms": 715 }, { - "t": "2026-10-13T13:13:00.000Z", + "t": "2026-10-13T00:20:00.000Z", "ok": true, - "ms": 772 + "ms": 683 }, { - "t": "2026-10-13T13:50:00.000Z", + "t": "2026-10-13T01:00:00.000Z", "ok": true, - "ms": 1131 + "ms": 643 + }, + { + "t": "2026-10-13T01:40:00.000Z", + "ok": true, + "ms": 641 + }, + { + "t": "2026-10-13T02:20:00.000Z", + "ok": true, + "ms": 609 + }, + { + "t": "2026-10-13T03:00:00.000Z", + "ok": true, + "ms": 606 + }, + { + "t": "2026-10-13T03:40:00.000Z", + "ok": true, + "ms": 720 + }, + { + "t": "2026-10-13T04:20:00.000Z", + "ok": true, + "ms": 711 + }, + { + "t": "2026-10-13T05:00:00.000Z", + "ok": true, + "ms": 598 + }, + { + "t": "2026-10-13T05:40:00.000Z", + "ok": true, + "ms": 729 + }, + { + "t": "2026-10-13T06:20:00.000Z", + "ok": true, + "ms": 676 + }, + { + "t": "2026-10-13T07:00:00.000Z", + "ok": true, + "ms": 691 + }, + { + "t": "2026-10-13T07:40:00.000Z", + "ok": true, + "ms": 696 + }, + { + "t": "2026-10-13T08:20:00.000Z", + "ok": true, + "ms": 677 + }, + { + "t": "2026-10-13T09:00:00.000Z", + "ok": true, + "ms": 969 + }, + { + "t": "2026-10-13T09:40:00.000Z", + "ok": true, + "ms": 669 + }, + { + "t": "2026-10-13T10:20:00.000Z", + "ok": true, + "ms": 840 + }, + { + "t": "2026-10-13T11:00:00.000Z", + "ok": true, + "ms": 735 + }, + { + "t": "2026-10-13T11:40:00.000Z", + "ok": true, + "ms": 693 + }, + { + "t": "2026-10-13T12:20:00.000Z", + "ok": true, + "ms": 793 + }, + { + "t": "2026-10-13T13:56:00.000Z", + "ok": true, + "ms": 1056 + }, + { + "t": "2026-10-13T14:33:00.000Z", + "ok": true, + "ms": 704 } ], "daily": { "2026-10-13": { - "total": 35, - "up": 35 + "total": 46, + "up": 46 } }, "meta": { @@ -454,16 +449,11 @@ }, "ur-795556828": { "status": "up", - "latency": 224, + "latency": 338, "code": null, "detail": "UptimeRobot:运行正常", - "lastCheck": "2026-10-13T13:49:00.000Z", + "lastCheck": "2026-10-13T14:34:00.000Z", "history": [ - { - "t": "2026-10-12T14:55:00.000Z", - "ok": true, - "ms": 225 - }, { "t": "2026-10-12T15:35:00.000Z", "ok": true, @@ -560,85 +550,90 @@ "ms": 229 }, { - "t": "2026-10-13T04:25:00.000Z", + "t": "2026-10-13T04:30:00.000Z", "ok": true, - "ms": 255 + "ms": 234 }, { - "t": "2026-10-13T05:05:00.000Z", + "t": "2026-10-13T05:10:00.000Z", "ok": true, - "ms": 150 + "ms": 181 }, { - "t": "2026-10-13T05:45:00.000Z", + "t": "2026-10-13T05:50:00.000Z", "ok": true, - "ms": 201 + "ms": 206 }, { - "t": "2026-10-13T06:25:00.000Z", + "t": "2026-10-13T06:30:00.000Z", "ok": true, - "ms": 232 + "ms": 241 }, { - "t": "2026-10-13T07:05:00.000Z", + "t": "2026-10-13T07:10:00.000Z", "ok": true, - "ms": 247 + "ms": 241 }, { - "t": "2026-10-13T07:45:00.000Z", + "t": "2026-10-13T07:50:00.000Z", "ok": true, - "ms": 559 + "ms": 512 }, { - "t": "2026-10-13T08:25:00.000Z", + "t": "2026-10-13T08:30:00.000Z", "ok": true, - "ms": 219 + "ms": 216 }, { - "t": "2026-10-13T09:05:00.000Z", + "t": "2026-10-13T09:10:00.000Z", "ok": true, - "ms": 230 + "ms": 256 }, { - "t": "2026-10-13T09:45:00.000Z", + "t": "2026-10-13T09:55:00.000Z", "ok": true, - "ms": 432 + "ms": 455 }, { - "t": "2026-10-13T10:25:00.000Z", + "t": "2026-10-13T10:35:00.000Z", "ok": true, - "ms": 341 + "ms": 372 }, { - "t": "2026-10-13T11:05:00.000Z", + "t": "2026-10-13T11:15:00.000Z", "ok": true, - "ms": 233 + "ms": 240 }, { - "t": "2026-10-13T11:45:00.000Z", + "t": "2026-10-13T11:55:00.000Z", "ok": true, - "ms": 396 + "ms": 261 }, { - "t": "2026-10-13T12:25:00.000Z", + "t": "2026-10-13T12:35:00.000Z", "ok": true, - "ms": 308 + "ms": 335 }, { - "t": "2026-10-13T13:13:00.000Z", + "t": "2026-10-13T13:15:00.000Z", "ok": true, - "ms": 347 + "ms": 413 }, { - "t": "2026-10-13T13:49:00.000Z", + "t": "2026-10-13T13:54:00.000Z", "ok": true, - "ms": 224 + "ms": 187 + }, + { + "t": "2026-10-13T14:34:00.000Z", + "ok": true, + "ms": 338 } ], "daily": { "2026-10-13": { - "total": 35, - "up": 35 + "total": 46, + "up": 46 } }, "meta": { @@ -665,16 +660,11 @@ }, "ur-792059414": { "status": "up", - "latency": 3462, + "latency": 2424, "code": null, "detail": "UptimeRobot:运行正常", - "lastCheck": "2026-10-13T13:50:00.000Z", + "lastCheck": "2026-10-13T14:34:00.000Z", "history": [ - { - "t": "2026-10-12T14:55:00.000Z", - "ok": true, - "ms": 3489 - }, { "t": "2026-10-12T15:40:00.000Z", "ok": true, @@ -796,50 +786,55 @@ "ms": 1861 }, { - "t": "2026-10-13T09:05:00.000Z", + "t": "2026-10-13T09:10:00.000Z", "ok": true, - "ms": 2001 + "ms": 1991 }, { - "t": "2026-10-13T09:45:00.000Z", + "t": "2026-10-13T09:55:00.000Z", "ok": true, - "ms": 1652 + "ms": 1597 }, { - "t": "2026-10-13T10:25:00.000Z", + "t": "2026-10-13T10:35:00.000Z", "ok": true, - "ms": 2264 + "ms": 3015 }, { - "t": "2026-10-13T11:05:00.000Z", + "t": "2026-10-13T11:15:00.000Z", "ok": true, - "ms": 2105 + "ms": 1965 }, { - "t": "2026-10-13T11:45:00.000Z", + "t": "2026-10-13T11:55:00.000Z", "ok": true, - "ms": 1562 + "ms": 2118 }, { - "t": "2026-10-13T12:25:00.000Z", + "t": "2026-10-13T12:35:00.000Z", "ok": true, - "ms": 2773 + "ms": 2557 }, { - "t": "2026-10-13T13:10:00.000Z", + "t": "2026-10-13T13:15:00.000Z", "ok": true, - "ms": 2536 + "ms": 1671 }, { - "t": "2026-10-13T13:50:00.000Z", + "t": "2026-10-13T13:55:00.000Z", "ok": true, - "ms": 3462 + "ms": 1908 + }, + { + "t": "2026-10-13T14:34:00.000Z", + "ok": true, + "ms": 2424 } ], "daily": { "2026-10-13": { - "total": 35, - "up": 35 + "total": 46, + "up": 46 } }, "meta": { @@ -866,16 +861,11 @@ }, "ur-798788113": { "status": "up", - "latency": 488, + "latency": 438, "code": null, "detail": "UptimeRobot:运行正常", - "lastCheck": "2026-10-13T13:50:00.000Z", + "lastCheck": "2026-10-13T14:35:00.000Z", "history": [ - { - "t": "2026-10-12T15:05:00.000Z", - "ok": true, - "ms": 439 - }, { "t": "2026-10-12T15:45:00.000Z", "ok": true, @@ -892,165 +882,170 @@ "ms": 409 }, { - "t": "2026-10-12T17:45:00.000Z", + "t": "2026-10-12T17:50:00.000Z", "ok": true, - "ms": 334 + "ms": 312 }, { - "t": "2026-10-12T18:25:00.000Z", + "t": "2026-10-12T18:30:00.000Z", "ok": true, "ms": 472 }, { - "t": "2026-10-12T19:05:00.000Z", + "t": "2026-10-12T19:10:00.000Z", "ok": true, - "ms": 370 + "ms": 421 }, { - "t": "2026-10-12T19:45:00.000Z", + "t": "2026-10-12T19:50:00.000Z", "ok": true, - "ms": 353 + "ms": 332 }, { - "t": "2026-10-12T20:25:00.000Z", + "t": "2026-10-12T20:30:00.000Z", "ok": true, - "ms": 274 + "ms": 310 }, { - "t": "2026-10-12T21:05:00.000Z", + "t": "2026-10-12T21:10:00.000Z", "ok": true, - "ms": 535 + "ms": 436 }, { - "t": "2026-10-12T21:45:00.000Z", + "t": "2026-10-12T21:50:00.000Z", "ok": true, - "ms": 340 + "ms": 338 }, { - "t": "2026-10-12T22:25:00.000Z", + "t": "2026-10-12T22:30:00.000Z", "ok": true, - "ms": 470 + "ms": 423 }, { - "t": "2026-10-12T23:05:00.000Z", + "t": "2026-10-12T23:10:00.000Z", "ok": true, - "ms": 245 + "ms": 247 }, { - "t": "2026-10-12T23:45:00.000Z", + "t": "2026-10-12T23:50:00.000Z", "ok": true, - "ms": 270 + "ms": 296 }, { - "t": "2026-10-13T00:25:00.000Z", + "t": "2026-10-13T00:30:00.000Z", "ok": true, - "ms": 244 + "ms": 239 }, { - "t": "2026-10-13T01:05:00.000Z", + "t": "2026-10-13T01:10:00.000Z", "ok": true, - "ms": 302 + "ms": 393 }, { - "t": "2026-10-13T01:45:00.000Z", - "ok": true, - "ms": 277 - }, - { - "t": "2026-10-13T02:25:00.000Z", - "ok": true, - "ms": 463 - }, - { - "t": "2026-10-13T03:05:00.000Z", - "ok": true, - "ms": 437 - }, - { - "t": "2026-10-13T03:45:00.000Z", - "ok": true, - "ms": 335 - }, - { - "t": "2026-10-13T04:25:00.000Z", - "ok": true, - "ms": 320 - }, - { - "t": "2026-10-13T05:05:00.000Z", - "ok": true, - "ms": 532 - }, - { - "t": "2026-10-13T05:45:00.000Z", - "ok": true, - "ms": 334 - }, - { - "t": "2026-10-13T06:25:00.000Z", - "ok": true, - "ms": 425 - }, - { - "t": "2026-10-13T07:05:00.000Z", + "t": "2026-10-13T01:50:00.000Z", "ok": true, "ms": 346 }, { - "t": "2026-10-13T07:45:00.000Z", + "t": "2026-10-13T02:30:00.000Z", "ok": true, - "ms": 278 + "ms": 441 }, { - "t": "2026-10-13T08:25:00.000Z", + "t": "2026-10-13T03:10:00.000Z", "ok": true, - "ms": 368 + "ms": 443 }, { - "t": "2026-10-13T09:05:00.000Z", + "t": "2026-10-13T03:50:00.000Z", "ok": true, - "ms": 381 + "ms": 382 }, { - "t": "2026-10-13T09:45:00.000Z", + "t": "2026-10-13T04:30:00.000Z", "ok": true, - "ms": 576 + "ms": 355 }, { - "t": "2026-10-13T10:25:00.000Z", + "t": "2026-10-13T05:10:00.000Z", "ok": true, - "ms": 513 + "ms": 414 }, { - "t": "2026-10-13T11:05:00.000Z", + "t": "2026-10-13T05:50:00.000Z", "ok": true, - "ms": 381 + "ms": 343 }, { - "t": "2026-10-13T11:45:00.000Z", + "t": "2026-10-13T06:30:00.000Z", "ok": true, - "ms": 1028 + "ms": 434 }, { - "t": "2026-10-13T12:25:00.000Z", + "t": "2026-10-13T07:10:00.000Z", "ok": true, - "ms": 601 + "ms": 444 }, { - "t": "2026-10-13T13:14:00.000Z", + "t": "2026-10-13T07:50:00.000Z", "ok": true, - "ms": 428 + "ms": 281 }, { - "t": "2026-10-13T13:50:00.000Z", + "t": "2026-10-13T08:30:00.000Z", "ok": true, - "ms": 488 + "ms": 453 + }, + { + "t": "2026-10-13T09:15:00.000Z", + "ok": true, + "ms": 333 + }, + { + "t": "2026-10-13T09:55:00.000Z", + "ok": true, + "ms": 602 + }, + { + "t": "2026-10-13T10:35:00.000Z", + "ok": true, + "ms": 519 + }, + { + "t": "2026-10-13T11:15:00.000Z", + "ok": true, + "ms": 325 + }, + { + "t": "2026-10-13T11:55:00.000Z", + "ok": true, + "ms": 933 + }, + { + "t": "2026-10-13T12:35:00.000Z", + "ok": true, + "ms": 424 + }, + { + "t": "2026-10-13T13:15:00.000Z", + "ok": true, + "ms": 460 + }, + { + "t": "2026-10-13T13:55:00.000Z", + "ok": true, + "ms": 328 + }, + { + "t": "2026-10-13T14:35:00.000Z", + "ok": true, + "ms": 438 } ], "daily": { "2026-10-13": { - "total": 35, - "up": 35 + "total": 46, + "up": 46 } }, "meta": { @@ -1090,7 +1085,7 @@ ], "daily": { "2026-10-13": { - "total": 35, + "total": 46, "up": 0 } }, diff --git a/scripts/verify-layout.cjs b/scripts/verify-layout.cjs new file mode 100644 index 0000000..3bae09f --- /dev/null +++ b/scripts/verify-layout.cjs @@ -0,0 +1,120 @@ +/** + * shiroko-monitor 布局回归:任何宽度、任何内容都不应出现横向滚动 + * + * 硬要求(用户明确):不允许出现「需要滚动才能看全」的情况。 + * 因此这里用**极端内容**(超长名称 / 超长 URL / 6 个分组 / 40 个目标)+ 13 种视口 × 2 种视图 + * 做扫描,断言页面与表格容器都不能有横向溢出。 + * + * 为什么要用极端内容:常规内容下很多撑破问题不会暴露(实测卡片视图在 320px 下 + * 曾因 grid 的 min-width:auto 被长 URL 撑到 801px,溢出 495px)。 + * + * 用法:node scripts/verify-layout.cjs + */ +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 ROOT = path.join(__dirname, '..'); +const PROBER = path.join(ROOT, 'prober', 'node', 'index.js'); +const CHROME = 'C:/Program Files/Google/Chrome/Application/chrome.exe'; +const PORT = 8893; +const WIDTHS = [320, 360, 390, 480, 600, 768, 820, 900, 1024, 1280, 1440, 1920, 2560]; + +const results = []; +const check = (name, pass, detail) => { + results.push({ name, pass }); + console.log(`${pass ? ' ✓' : ' ✗'} ${name}${detail ? ` — ${detail}` : ''}`); +}; +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' }; + +// 极端配置:长名称 + 长 URL + 多分组 + 多目标 +function hostileConfig() { + const groups = ['超长分组名称测试一二三四五', '公共服务', '基础设施与内网设备', 'UptimeRobot', '外部依赖', '测试用极端分组名']; + const monitors = []; + for (let i = 0; i < 40; i++) { + monitors.push({ + id: `hostile-${i}`, + name: i % 3 === 0 ? `这是一个非常非常长的监控目标名称用于测试极端情况下的布局表现与截断行为-${i + 1}` : `目标-${i + 1}`, + group: groups[i % groups.length], + type: 'http', + url: i % 2 === 0 ? `https://very-long-subdomain-name-for-testing-layout.example.com/very/long/path/segment?with=query&and=more&even=more&idx=${i}` : `https://s${i}.example.com`, + expectStatus: [200], + interval: 60, + timeout: 5, + }); + } + return monitors; +} + +function runOnce(args) { + return new Promise((resolve) => { + const child = spawn(process.execPath, [PROBER, ...args], { stdio: 'ignore' }); + child.on('exit', (code) => resolve(code)); + }); +} + +async function main() { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'shiroko-layout-')); + const cfg = path.join(tmp, 'monitors.json'); + fs.cpSync(path.join(ROOT, 'public'), tmp, { recursive: true }); + const base = JSON.parse(fs.readFileSync(cfg, 'utf8')); + base.settings.dataFilter = 'config'; + base.monitors = hostileConfig(); + fs.writeFileSync(cfg, `${JSON.stringify(base, null, 2)}\n`); + await runOnce(['--once', '--demo', `--config=${cfg}`, `--local-config=${path.join(tmp, 'none.json')}`, `--out=${path.join(tmp, 'status.json')}`]); + console.log(`极端内容:${base.monitors.length} 个目标 / 6 个分组,视口 ${WIDTHS.length} 种 × 2 视图\n`); + + const server = http.createServer((req, res) => { + const rel = decodeURIComponent(req.url.split('?')[0]); + const file = path.join(tmp, rel === '/' ? 'index.html' : rel); + fs.readFile(file, (err, data) => { + if (err) { + res.writeHead(404); + res.end(); + return; + } + res.writeHead(200, { 'content-type': MIME[path.extname(file)] ?? 'application/octet-stream' }); + res.end(data); + }); + }); + await new Promise((r) => server.listen(PORT, r)); + + const browser = await chromium.launch({ executablePath: CHROME, args: ['--no-sandbox'] }); + let worst = null; + try { + for (const view of ['table', 'cards']) { + for (const width of WIDTHS) { + const page = await browser.newPage({ viewport: { width, height: 900 } }); + await page.goto(`http://localhost:${PORT}/?source=static&view=${view}`, { waitUntil: 'domcontentloaded' }); + await page.waitForSelector('.card, table.monitor-table', { timeout: 20000 }); + await page.waitForTimeout(400); + const r = await page.evaluate(() => ({ + page: document.documentElement.scrollWidth - document.documentElement.clientWidth, + wrap: (() => { const w = document.querySelector('.table-wrap'); return w ? w.scrollWidth - w.clientWidth : 0; })(), + grid: (() => { const g = document.querySelector('.grid'); return g ? g.scrollWidth - g.clientWidth : 0; })(), + })); + const bad = Math.max(r.page, r.wrap, r.grid); + if (!worst || bad > worst.bad) worst = { bad, view, width, r }; + check(`${String(width).padStart(4)}px ${view} 无横向溢出`, bad <= 0, `页面 ${r.page} / 表格 ${r.wrap} / 网格 ${r.grid}`); + await page.close(); + } + } + } finally { + await browser.close(); + server.close(); + fs.rmSync(tmp, { recursive: true, force: true }); + } + + if (worst && worst.bad > 0) console.log(`\n最大溢出:${worst.width}px ${worst.view} → ${worst.bad}px`); + const failed = results.filter((x) => !x.pass); + console.log(`\n结果:${results.length - failed.length}/${results.length} 通过`); + if (failed.length) process.exit(1); +} + +main().catch((err) => { + console.error('FAILED:', err.stack || err.message); + process.exit(1); +});