feat: v0.12.0 —— 表格每行绝对等高 + 管理 WebUI(2FA 绑定才能进入)
① 每行绝对等高 - 行高与内容长度彻底解耦:新增 --row-h: 72px 作为唯一基准 - 名称列改为「文本区限两行 + 徽标区固定单行」两段式;文本区用 .clamp2 - 关键点:必须用 height 而非 max-height —— 后者会让短内容缩成一行、 长内容撑到两行,反而产生 66px/71px 两种行高(实测踩过) - 分组标题行用 :not(.group-row) 排除在数据行规则之外 - 事件表与监控表格共用同一个 --row-h,两张表视觉上也严格对齐 - 完整值仍可通过悬停 title 查看(已加断言强制所有截断单元格带 title) - 卡片视图同样等高(13 视口 × 250px 全一致) ② 管理 WebUI + 2FA - 新增 /admin 与 /api/admin/*;未绑定 2FA 时除绑定流程外一律 403 - 零依赖实现 TOTP(RFC 6238),用 RFC 4226/6238 官方测试向量验证(16 个向量) - 内置 QR 码生成器(byte 模式 / ECC L / 版本 1-10,GF(256) RS 纠错 + 掩码评分), 用独立解码器实测:版本 1 与版本 6 均能读出逐字节一致的内容 - 绑定流程:口令 → 生成密钥(扫码/手输/otpauth 链接)→ 验证 6 位码 → 10 枚恢复码; 完成后凭本次验证码直接换取会话 - 安全:会话 id 32 字节随机数且服务端只存哈希;HttpOnly+SameSite=Strict; 30 分钟空闲/2 小时最长;同时间步的码防重放;恢复码一次性; 登录失败按 IP 指数退避锁定;不解析 X-Forwarded-For - 管理功能:运行状态、探测范围、展示口径、维护窗口增删、立即重探、通知自检、 目标只读列表、最近事件 - 所有写操作只落 local.monitors.json(私有不入库),绝不改 public/monitors.json 过程中修掉三个真 bug: 1. 会话 cookie 的 Path=/admin 覆盖不到 /api/admin/*,登录 200 但后续全部 401 2. 恢复码登录被 verifyTotp 的 invalid-format 提前挡掉(恢复码非 6 位数字) 3. 超大请求体用 req.destroy() 硬断连接,客户端只看到 fetch failed 拿不到状态码 测试:新增 scripts/verify-admin.cjs(102 项),全项目共 7 套 274 项全部通过
This commit is contained in:
+44
-8
@@ -21,6 +21,11 @@
|
||||
--mono: ui-monospace, SFMono-Regular, "Cascadia Code", Consolas, "Courier New", monospace;
|
||||
--sans: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
||||
--shadow: 0 1px 2px rgb(0 0 0 / 0.04);
|
||||
/* 表格行高基准:固定像素,行高与内容长度彻底解耦(任意长度内容下行高都相同)。
|
||||
72px = 名称两行(36) + 徽标行(18) + 上下内边距(16),监控表与事件表共用同一个值,
|
||||
两张表因此在视觉上也严格对齐。 */
|
||||
--row-h: 72px;
|
||||
--group-row-h: 32px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
@@ -368,6 +373,11 @@ body[data-view="table"] .grid { display: block; }
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
}
|
||||
/* 行高基准:所有数据行恒为 --row-h,与内容长度彻底解耦。
|
||||
内容再长也不会把某一行撑高(文本列各自限行截断 + 单元格 overflow:hidden)。
|
||||
分组标题行另设 --group-row-h,用 :not() 排除掉,否则同样会被这条规则套上。 */
|
||||
.monitor-table tbody tr:not(.group-row) { height: var(--row-h); }
|
||||
.group-row td { height: var(--group-row-h); }
|
||||
/* 表格列宽:状态 / 服务 / 目标 / 延迟 / 结果 / 最近检测 / 心率 / 90天可用率 = 100%
|
||||
地址与结果列给足比例,避免长 URL、长名称被截断 */
|
||||
.monitor-table th:nth-child(1) { width: 7%; }
|
||||
@@ -390,34 +400,56 @@ body[data-view="table"] .grid { display: block; }
|
||||
background: var(--bg);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.monitor-table td { padding: 9px 12px; border-bottom: 1px solid var(--border); vertical-align: middle; overflow: hidden; line-height: 1.4; }
|
||||
.monitor-table td { padding: 8px 12px; border-bottom: 1px solid var(--border); vertical-align: middle; overflow: hidden; line-height: 1.4; }
|
||||
/* 所有行统一基线:可用率的 mini 条是 inline-flex,其基线会把行盒撑高十几像素,
|
||||
导致带条的行比不带的行高出一截(实测 58px vs 43px) */
|
||||
.monitor-table td > * { vertical-align: middle; }
|
||||
.u-mini { display: inline-flex; align-items: center; gap: 6px; vertical-align: middle; }
|
||||
/* 限行截断:文本再多也只占固定的两行,行高因此与内容长度彻底解耦。
|
||||
height 而非 max-height —— 只有 1 行内容时也照样占满 2 行,行高才恒定;
|
||||
max-height 会让短内容缩成 1 行、长内容撑到 2 行,反而出现两种行高。
|
||||
-webkit-line-clamp 是 Chrome/Safari/Edge 的实现,height + overflow 兜底 Firefox。 */
|
||||
.clamp2 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
height: 2.8em;
|
||||
}
|
||||
.monitor-table tbody tr[data-status="pending"] { background: color-mix(in srgb, var(--pending) 6%, transparent); }
|
||||
.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); }
|
||||
.group-row td { padding: 7px 12px; font-size: 12px; font-weight: 650; color: var(--text-2); background: var(--bg); }
|
||||
.group-row td { padding: 7px 12px; font-size: 12px; font-weight: 650; color: var(--text-2); background: var(--bg); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
|
||||
.t-state { white-space: nowrap; }
|
||||
.t-state .dot { display: inline-block; margin-right: 6px; vertical-align: middle; }
|
||||
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: normal; overflow-wrap: anywhere; }
|
||||
.t-name .type-badge { margin-left: 8px; }
|
||||
/* 名称列:文本区限两行,徽标区固定单行高度 —— 整格高度恒定 */
|
||||
.t-name { white-space: normal; }
|
||||
.t-name .clamp2 { overflow-wrap: anywhere; }
|
||||
.t-tags {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
height: 16px;
|
||||
margin-top: 2px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.t-name .type-badge { margin-left: 0; }
|
||||
.t-target {
|
||||
font-family: var(--mono);
|
||||
font-size: 11.5px;
|
||||
line-height: 1.45;
|
||||
color: var(--text-2);
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-all;
|
||||
}
|
||||
.t-num { font-variant-numeric: tabular-nums; white-space: normal; overflow-wrap: anywhere; }
|
||||
.t-target .clamp2 { overflow-wrap: anywhere; word-break: break-all; }
|
||||
.t-num { font-variant-numeric: tabular-nums; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
|
||||
/* 证书 / 维护徽标:出现在名称旁,不占独立列 */
|
||||
.tls-badge, .maint-badge {
|
||||
@@ -469,6 +501,7 @@ tr[data-status="pending"] .t-state { color: var(--pending-text); }
|
||||
/* 事件表:显式声明与监控表格相同的观感(不共用 .monitor-table 类,
|
||||
避免被其他按类名统计/定位的逻辑误算),列宽与类型徽标单独定义 */
|
||||
.events-table { width: 100%; table-layout: fixed; border-collapse: collapse; font-size: 13px; }
|
||||
.events-table tbody tr { height: var(--row-h); }
|
||||
.events-table th {
|
||||
padding: 10px 12px;
|
||||
text-align: left;
|
||||
@@ -481,7 +514,7 @@ tr[data-status="pending"] .t-state { color: var(--pending-text); }
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.events-table td { padding: 11px 12px; border-bottom: 1px solid var(--border); vertical-align: middle; line-height: 1.4; overflow: hidden; }
|
||||
.events-table td { padding: 10px 12px; border-bottom: 1px solid var(--border); vertical-align: middle; line-height: 1.4; overflow: hidden; }
|
||||
.events-table tbody tr:last-child td { border-bottom: 0; }
|
||||
.events-table tbody tr:hover { background: color-mix(in srgb, var(--accent) 5%, transparent); }
|
||||
.events-table tbody tr[data-status="down"] { background: color-mix(in srgb, var(--down) 6%, transparent); }
|
||||
@@ -505,6 +538,9 @@ 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); }
|
||||
/* 事件表的长文本同样限两行,保证与监控表格一致的等高行 */
|
||||
.ev-name, .ev-detail { white-space: normal; }
|
||||
.ev-name > .clamp2, .ev-detail > .clamp2 { overflow-wrap: anywhere; }
|
||||
.events-empty { margin: 0; padding: 12px; font-size: 12px; color: var(--text-2); background: var(--surface); border: 1px dashed var(--border); border-radius: 12px; }
|
||||
|
||||
@media (max-width: 640px) {
|
||||
|
||||
Reference in New Issue
Block a user