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 @@
-
+
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);
+});