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:
@@ -95,10 +95,23 @@ async function main() {
|
||||
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; })(),
|
||||
// 表格视图额外要求绝对等高:40 个目标、6 个分组、名称与 URL 长短悬殊,
|
||||
// 行高仍须只有一个取值(硬要求:每行绝对等高)
|
||||
rowHs: document.body.dataset.view === 'table'
|
||||
? [...new Set([...document.querySelectorAll('table.monitor-table tbody tr:not(.group-row)')]
|
||||
.map((tr) => Math.round(tr.getBoundingClientRect().height)))]
|
||||
: null,
|
||||
cardHs: document.body.dataset.view === 'cards'
|
||||
? [...new Set([...document.querySelectorAll('.card')].map((c) => Math.round(c.getBoundingClientRect().height)))]
|
||||
: null,
|
||||
}));
|
||||
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}`);
|
||||
const extra = view === 'table'
|
||||
? (r.rowHs?.length === 1 ? `行高 ${r.rowHs[0]}px` : `行高 ${(r.rowHs ?? []).join('/')}`)
|
||||
: (r.cardHs?.length === 1 ? `卡片高 ${r.cardHs[0]}px` : `卡片高 ${(r.cardHs ?? []).join('/')}`);
|
||||
check(`${String(width).padStart(4)}px ${view} 无横向溢出且等高`, bad <= 0 && ((view === 'table' ? r.rowHs : r.cardHs) ?? []).length === 1,
|
||||
`页面 ${r.page} / 表格 ${r.wrap} / 网格 ${r.grid},${extra}`);
|
||||
await page.close();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user