fix: 表格完整显示 + 事件区块样式重做 + 探测范围可选(probeScope)
1) 表格显示不全 table-layout:fixed 保证不横向滚动后,列内仍是 nowrap,实测长 URL 被截 388px、名称截 219px、可用率条截 35px(1440 宽共 14 个单元格)。 改为允许换行(地址 break-all)并重排列宽 —— 数据完整且仍零横向滚动。 2) 最近事件完全没样式 根因:上次追加 CSS 的命令是 `node --check app.js && cat >> style.css`, --check 报错导致 && 短路,CSS 从未写入(只有 HTML 裸奔)。 本次用编辑器写入;同时重做为时间线样式(竖线 + 类型着色圆点 + 左描边)、 名称列按内容自适应(消除大片空白)、默认折叠最近 8 条可展开全部。 3) dataFilter=uptimerobot 时仍在探测本地目标 新增 settings.probeScope(all|config|uptimerobot),不填则跟随 dataFilter; 显式设 all 可保留探测。启动日志打印当前口径与范围。 顺带修复两个连带问题: - UR 镜像目标此前不产生事件也不告警(evaluateRound 只遍历本地目标), 配 uptimerobot 口径等于收不到任何提醒 —— 现在两类目标统一纳入 - 去抖计数被评估频率放大:evaluateRound 每秒执行,原实现每次评估都 failStreak+1,持续离线目标 1 秒即达阈值告警。改为按探测时间戳去重, 且无状态变化时不再反复落盘 测试:verify-views [J] 扩到 11 条(长内容不截断/仍不滚动/时间线布局/ 类型着色/折叠展开),verify-notify 新增 [D3] 4 条(范围跟随、不再探测本地、 显式 all 恢复、与展示口径解耦)+ 去抖计数断言。六套 170 项全通过。
This commit is contained in:
@@ -298,6 +298,9 @@ async function main() {
|
||||
check('告警邮件含目标名称与详情', /必然离线的 TCP/.test(downDecoded.body) && /【故障】/.test(downDecoded.subject), `主题「${downDecoded.subject}」正文 ${downDecoded.body.length} 字符`);
|
||||
const events2 = readEvents(dataDir);
|
||||
check('事件时间线记录 down', events2.some((e) => e.type === 'down' && e.id === 'down-tcp'), JSON.stringify(events2.map((e) => e.type)));
|
||||
// 去抖计数必须与「探测轮次」绑定:评估每秒跑一次,不能每秒累加
|
||||
const ns2 = JSON.parse(fs.readFileSync(path.join(dataDir, 'notify-state.json'), 'utf8'));
|
||||
check('连续失败计数=探测轮数(不被每秒评估放大)', ns2.monitors['down-tcp'].failStreak === 2, `failStreak=${ns2.monitors['down-tcp'].failStreak},期望 2`);
|
||||
|
||||
await runOnce(['--once', ...common]);
|
||||
const mailAfter3 = smtp.received.length - beforeMail;
|
||||
@@ -335,6 +338,33 @@ async function main() {
|
||||
const snap2 = JSON.parse(fs.readFileSync(outFile2, 'utf8'));
|
||||
check('快照中目标带维护标记', snap2.monitors['down-tcp'].maintenance?.note === '计划维护', '');
|
||||
|
||||
/* ---------- D3. 探测范围可选(probeScope) ---------- */
|
||||
console.log('\n[D3] 探测范围(probeScope)');
|
||||
const tmp3 = fs.mkdtempSync(path.join(os.tmpdir(), 'shiroko-scope-'));
|
||||
fs.cpSync(path.join(ROOT, 'public'), tmp3, { recursive: true });
|
||||
const { file: file3 } = writeConfig(tmp3);
|
||||
const scopeCfg = JSON.parse(fs.readFileSync(file3, 'utf8'));
|
||||
scopeCfg.settings.dataFilter = 'uptimerobot'; // 只展示 UR 口径
|
||||
scopeCfg.settings.uptimeRobot = { enabled: false }; // 本测试不连 UR,只看本地探测是否发生
|
||||
scopeCfg.monitors.push({ id: 'local-must-not-run', name: '不该被探测的本地目标', group: '测试', type: 'tcp', host: '127.0.0.1', port: 1, interval: 60, timeout: 3 });
|
||||
fs.writeFileSync(file3, `${JSON.stringify(scopeCfg, null, 2)}\n`);
|
||||
const scopeArgs = (cfgFile, out, data) => ['--once', `--config=${cfgFile}`, `--local-config=${cfgFile}`, `--out=${out}`, `--data-dir=${data}`];
|
||||
|
||||
const outA = path.join(tmp3, 'a.json');
|
||||
const resA = await runOnce(scopeArgs(file3, outA, path.join(tmp3, 'da')));
|
||||
const snapA = JSON.parse(fs.readFileSync(outA, 'utf8'));
|
||||
check('dataFilter=uptimerobot 时探测范围自动跟随', snapA.probeScope === 'uptimerobot', `probeScope=${snapA.probeScope}`);
|
||||
check('跟随口径时不再探测本地配置目标', !/local-must-not-run/.test(resA.out), resA.out.split('\n').filter((l) => /\[up\]|\[down\]/.test(l)).join(' | ').slice(0, 80) || '本地探测日志为空');
|
||||
|
||||
// 对照:显式 probeScope=all 时应恢复本地探测(证明是「可选」而非「砍掉」)
|
||||
scopeCfg.settings.probeScope = 'all';
|
||||
fs.writeFileSync(file3, `${JSON.stringify(scopeCfg, null, 2)}\n`);
|
||||
const outB = path.join(tmp3, 'b.json');
|
||||
const resB = await runOnce(scopeArgs(file3, outB, path.join(tmp3, 'db')));
|
||||
const snapB = JSON.parse(fs.readFileSync(outB, 'utf8'));
|
||||
check('显式 probeScope=all 时恢复本地探测', snapB.probeScope === 'all' && /local-must-not-run/.test(resB.out), `probeScope=${snapB.probeScope}`);
|
||||
check('探测范围与展示口径解耦(all + uptimerobot 展示)', snapB.dataFilter === 'uptimerobot' && snapB.probeScope === 'all', `filter=${snapB.dataFilter} scope=${snapB.probeScope}`);
|
||||
|
||||
/* ---------- E. 证书 ---------- */
|
||||
console.log('\n[E] 证书');
|
||||
check('tlsTargetOf 识别 https 目标', tlsTargetOf({ type: 'http', url: 'https://www.example.com:8443/x' }).port === 8443, '');
|
||||
|
||||
@@ -299,6 +299,65 @@ async function main() {
|
||||
await dp.waitForTimeout(500);
|
||||
const dt = await dp.evaluate(() => document.querySelector('tr[data-status] .t-uptime')?.title ?? '');
|
||||
check('表格可用率 tooltip 含 90 天日均延迟', /90 天日均延迟 \d+ ms/.test(dt), dt.slice(0, 60));
|
||||
|
||||
// ---- 极端内容:表格不得把数据截断,同时不得出现横向滚动 ----
|
||||
const statusPath = path.join(tmpDir, 'status.json');
|
||||
const snap = JSON.parse(fs.readFileSync(statusPath, 'utf8'));
|
||||
const ids = Object.keys(snap.monitors);
|
||||
let injected = 0;
|
||||
for (const id of ids.slice(0, 2)) {
|
||||
snap.monitors[id].name = `这是一个相当长的监控目标名称用于验证表格完整显示-${id}`;
|
||||
injected++;
|
||||
}
|
||||
// 注入事件,验证事件区块的时间线样式与折叠
|
||||
snap.events = Array.from({ length: 12 }, (_, i) => ({
|
||||
t: new Date(Date.now() - i * 60000).toISOString(),
|
||||
id: ids[0],
|
||||
name: snap.monitors[ids[0]].name,
|
||||
type: i % 2 ? 'down' : 'up',
|
||||
detail: i % 2 ? 'ETIMEDOUT' : '已恢复',
|
||||
maintenance: false,
|
||||
}));
|
||||
fs.writeFileSync(statusPath, JSON.stringify(snap));
|
||||
await dp.goto('http://localhost:8894/?source=static&view=table', { waitUntil: 'domcontentloaded' });
|
||||
await dp.waitForSelector('table.monitor-table tbody tr[data-status]', { timeout: 20000 });
|
||||
await dp.waitForTimeout(900);
|
||||
const clip = await dp.evaluate(() => {
|
||||
let clipped = 0;
|
||||
let worst = 0;
|
||||
for (const tr of document.querySelectorAll('tr[data-status]')) {
|
||||
for (const td of tr.querySelectorAll('td')) {
|
||||
const d = td.scrollWidth - td.clientWidth;
|
||||
if (d > 1) { clipped++; worst = Math.max(worst, d); }
|
||||
}
|
||||
}
|
||||
const wrap = document.querySelector('.table-wrap');
|
||||
return { clipped, worst, pageOverflow: document.documentElement.scrollWidth - document.documentElement.clientWidth, tableOverflow: wrap ? wrap.scrollWidth - wrap.clientWidth : 0 };
|
||||
});
|
||||
check(`长名称/长地址不再被截断(注入 ${injected} 个)`, clip.clipped === 0, `截断单元格 ${clip.clipped} 个,最大 ${clip.worst}px`);
|
||||
check('表格仍无横向滚动', clip.pageOverflow <= 0 && clip.tableOverflow <= 0, `页面 ${clip.pageOverflow} / 表格 ${clip.tableOverflow}`);
|
||||
const evr = await dp.evaluate(() => {
|
||||
const ev = document.querySelector('.ev');
|
||||
const cs = ev ? getComputedStyle(ev) : null;
|
||||
const dot = ev ? getComputedStyle(ev, '::before') : null;
|
||||
const more = document.getElementById('events-more');
|
||||
return {
|
||||
shown: document.querySelectorAll('.ev').length,
|
||||
grid: cs?.display ?? 'none',
|
||||
dotVisible: dot ? dot.width !== '0px' && dot.backgroundColor !== 'rgba(0, 0, 0, 0)' : false,
|
||||
leftBorder: cs?.borderLeftWidth ?? '',
|
||||
moreHidden: more?.hidden ?? true,
|
||||
moreText: more?.textContent ?? '',
|
||||
count: document.getElementById('events-count')?.textContent ?? '',
|
||||
};
|
||||
});
|
||||
check('事件区块使用时间线布局(grid + 圆点)', evr.grid === 'grid' && evr.dotVisible === true, `display=${evr.grid} 圆点=${evr.dotVisible}`);
|
||||
check('事件行按类型着色(左描边)', evr.leftBorder === '3px', evr.leftBorder);
|
||||
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);
|
||||
const evExpanded = await dp.evaluate(() => ({ shown: document.querySelectorAll('.ev').length, text: document.getElementById('events-more').textContent }));
|
||||
check('展开后显示全部事件', evExpanded.shown === 12 && evExpanded.text === '收起', `显示 ${evExpanded.shown} 条`);
|
||||
await dp.close();
|
||||
await new Promise((r) => demoServer.close(r));
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user