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:
+73
-16
@@ -37,7 +37,7 @@ const CONCURRENCY = 8;
|
||||
const WRITE_DEBOUNCE_MS = 300;
|
||||
const DEFAULT_INTERVAL_SEC = 60;
|
||||
const PORT = Number(process.env.PORT || 8787);
|
||||
const VERSION = '0.11.0';
|
||||
const VERSION = '0.11.1';
|
||||
const UR_DEFAULT_POLL_SEC = 300;
|
||||
const UR_MIN_REFRESH_GAP_MS = 60000;
|
||||
|
||||
@@ -295,7 +295,7 @@ async function probeDue(runtime, force = false) {
|
||||
const { config, state } = runtime;
|
||||
const now = Date.now();
|
||||
const due = [];
|
||||
for (const m of config.monitors) {
|
||||
for (const m of localProbeTargets(config)) {
|
||||
const s = state.get(m.id);
|
||||
if (!s || s.inFlight) continue;
|
||||
// UptimeRobot 镜像目标由其自身节点探测,本探针只拉取结果
|
||||
@@ -408,7 +408,7 @@ const EVENTS_PATH = () => path.join(DATA_DIR, 'events.json');
|
||||
|
||||
function monitorNotifyState(runtime, id) {
|
||||
const list = runtime.notifyState.monitors;
|
||||
if (!list[id]) list[id] = { lastStatus: null, failStreak: 0, notifiedTls: [], tlsValidTo: null };
|
||||
if (!list[id]) list[id] = { lastStatus: null, failStreak: 0, lastCheckedAt: null, notifiedTls: [], tlsValidTo: null };
|
||||
return list[id];
|
||||
}
|
||||
|
||||
@@ -423,12 +423,28 @@ async function evaluateRound(runtime) {
|
||||
const notify = notifyConfig(runtime.config);
|
||||
const events = [];
|
||||
const pendingNotifications = [];
|
||||
let changed = false;
|
||||
|
||||
for (const m of runtime.config.monitors) {
|
||||
// 事件与告警覆盖两类目标:本地探测目标 + UptimeRobot 镜像目标
|
||||
// (UR 镜像目标同样会发生状态翻转,只拉数据不告警等于「配了 uptimerobot 口径就收不到任何提醒」)
|
||||
const targets = localProbeTargets(runtime.config).map((m) => ({ id: m.id, name: m.name, target: m.url ?? `${m.host}:${m.port}` }));
|
||||
for (const [id, s] of runtime.state) {
|
||||
if (s.monitor?.source !== 'uptimerobot') continue;
|
||||
targets.push({ id, name: s.monitor.name, target: s.monitor.url ?? '', source: 'uptimerobot' });
|
||||
}
|
||||
|
||||
for (const m of targets) {
|
||||
const r = runtime.state.get(m.id)?.lastResult;
|
||||
if (!r || r.status === 'pending') continue;
|
||||
const st = monitorNotifyState(runtime, m.id);
|
||||
st.failStreak = r.status === 'down' ? (st.failStreak ?? 0) + 1 : 0;
|
||||
// 去抖计数只应该跟着「新的探测结果」走:本函数每秒被调用,若每次评估都累加,
|
||||
// 一个持续离线的目标会在 1 秒内就凑够「连续失败 N 次」,阈值形同虚设
|
||||
const stamp = r.checkedAt ?? null;
|
||||
if (st.lastCheckedAt !== stamp) {
|
||||
st.lastCheckedAt = stamp;
|
||||
st.failStreak = r.status === 'down' ? (st.failStreak ?? 0) + 1 : 0;
|
||||
changed = true;
|
||||
}
|
||||
const maintenance = activeMaintenanceFor(runtime.config, m.id);
|
||||
const at = new Date().toISOString();
|
||||
|
||||
@@ -436,27 +452,30 @@ async function evaluateRound(runtime) {
|
||||
// 去抖:连续失败达到阈值才算确认故障(阈值 1 表示立即告警)
|
||||
if (st.lastStatus === 'down' || st.failStreak < notify.failureThreshold) continue;
|
||||
st.lastStatus = 'down';
|
||||
changed = true;
|
||||
st.lastNotifiedAt = at;
|
||||
events.push({ t: at, id: m.id, name: m.name, type: 'down', detail: r.detail ?? null, maintenance: Boolean(maintenance) });
|
||||
if (!maintenance) {
|
||||
pendingNotifications.push({
|
||||
subject: `【故障】${m.name} 连接失败`,
|
||||
text: `目标:${m.name}\n地址:${m.url ?? `${m.host}:${m.port}`}\n时间:${at}\n详情:${r.detail ?? '无'}\n连续失败:${st.failStreak} 次\n\n—— shiroko-monitor`,
|
||||
text: `目标:${m.name}\n地址:${m.target}\n时间:${at}\n详情:${r.detail ?? '无'}\n连续失败:${st.failStreak} 次\n\n—— shiroko-monitor`,
|
||||
});
|
||||
}
|
||||
} else if (r.status === 'up') {
|
||||
if (st.lastStatus === null) {
|
||||
st.lastStatus = 'up'; // 首次观测不产生「恢复」噪音
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
if (st.lastStatus === 'down') {
|
||||
st.lastStatus = 'up';
|
||||
st.lastNotifiedAt = at;
|
||||
changed = true;
|
||||
events.push({ t: at, id: m.id, name: m.name, type: 'up', detail: '已恢复', maintenance: Boolean(maintenance) });
|
||||
if (notify.recoveryNotify && !maintenance) {
|
||||
pendingNotifications.push({
|
||||
subject: `【恢复】${m.name} 已恢复正常`,
|
||||
text: `目标:${m.name}\n地址:${m.url ?? `${m.host}:${m.port}`}\n恢复时间:${at}\n延迟:${r.latency ?? '—'} ms\n\n—— shiroko-monitor`,
|
||||
text: `目标:${m.name}\n地址:${m.target}\n恢复时间:${at}\n延迟:${r.latency ?? '—'} ms\n\n—— shiroko-monitor`,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -477,9 +496,10 @@ async function evaluateRound(runtime) {
|
||||
};
|
||||
const res = await notifyAll(notify, message, { logger: console });
|
||||
runtime.notifyState.lastResult = { at: new Date().toISOString(), ...res };
|
||||
changed = true;
|
||||
}
|
||||
// 无论是否发通知都要落盘:否则「连续失败计数」会丢,重启或下一轮就达不到阈值
|
||||
saveNotifyState(runtime);
|
||||
// 只在状态真变化时落盘:tick 会每秒调用本函数,无变化时不写文件
|
||||
if (changed) saveNotifyState(runtime);
|
||||
}
|
||||
|
||||
// 证书检查:按 checkIntervalSec 逐个握手,跨阈值时告警(同一张证书每个阈值只告一次)
|
||||
@@ -490,7 +510,7 @@ async function checkCertificates(runtime) {
|
||||
const events = [];
|
||||
const messages = [];
|
||||
|
||||
for (const m of runtime.config.monitors) {
|
||||
for (const m of localProbeTargets(runtime.config)) {
|
||||
const target = tlsTargetOf(m);
|
||||
if (!target) continue;
|
||||
const prev = runtime.tls.get(m.id);
|
||||
@@ -554,6 +574,26 @@ function dataFilter(config) {
|
||||
return ['both', 'config', 'uptimerobot'].includes(f) ? f : 'both';
|
||||
}
|
||||
|
||||
// 探测范围(是否真的去探本地配置里的目标):
|
||||
// 显式设置 settings.probeScope 优先(all | config | uptimerobot)
|
||||
// 未设置时**跟随 dataFilter** —— 面板只要一个来源的数据,就没必要再把另一个来源的目标探测一遍
|
||||
// (既省资源,也避免不必要的对外请求;想保留探测可显式设 "all")
|
||||
function probeScopeSetting(config) {
|
||||
const p = config.settings?.probeScope;
|
||||
if (['all', 'config', 'uptimerobot'].includes(p)) return p;
|
||||
const f = dataFilter(config);
|
||||
return f === 'both' ? 'all' : f;
|
||||
}
|
||||
|
||||
function externalSyncEnabled(config) {
|
||||
return probeScopeSetting(config) !== 'config';
|
||||
}
|
||||
|
||||
// 本轮需要本地探测的目标(UR 镜像目标不在此列,它们由 UR 节点探测、这里只拉结果)
|
||||
function localProbeTargets(config) {
|
||||
return probeScopeSetting(config) === 'uptimerobot' ? [] : config.monitors;
|
||||
}
|
||||
|
||||
/* ---------- 服务端更新节奏 ---------- */
|
||||
|
||||
// 快照里回报「数据最后变更时刻」与「服务端产出新数据的节奏」,供面板把倒计时对齐到服务端,
|
||||
@@ -677,6 +717,8 @@ function snapshot(runtime, demo = false) {
|
||||
updateIntervalSec: updateIntervalSec(config, filter),
|
||||
// 让面板与服务端共用同一份过滤口径,避免私有配置覆盖后两边不一致
|
||||
dataFilter: filter,
|
||||
// 探测范围(本地目标 / 外部数据源),面板据此显示「数据源」与是否停止本地探测
|
||||
probeScope: probeScopeSetting(config),
|
||||
stats: {
|
||||
total: entries.length,
|
||||
up,
|
||||
@@ -698,9 +740,14 @@ function snapshot(runtime, demo = false) {
|
||||
lastResult: runtime.notifyState?.lastResult ?? null,
|
||||
};
|
||||
})(),
|
||||
events: (runtime.events ?? []).slice(-EVENT_IN_SNAPSHOT).reverse().map((e) => (redact
|
||||
? { t: e.t, id: e.id, type: e.type, name: e.name, detail: e.type === 'down' ? '异常' : e.type === 'up' ? '恢复' : e.detail, maintenance: e.maintenance }
|
||||
: e)),
|
||||
// 只下发当前展示口径内目标的事件:切到 uptimerobot 口径后不该再看到「已经不管的目标」的旧事件
|
||||
events: (runtime.events ?? [])
|
||||
.filter((e) => monitors[e.id])
|
||||
.slice(-EVENT_IN_SNAPSHOT)
|
||||
.reverse()
|
||||
.map((e) => (redact
|
||||
? { t: e.t, id: e.id, type: e.type, name: e.name, detail: e.type === 'down' ? '异常' : e.type === 'up' ? '恢复' : e.detail, maintenance: e.maintenance }
|
||||
: e)),
|
||||
monitors,
|
||||
};
|
||||
}
|
||||
@@ -987,7 +1034,7 @@ async function main() {
|
||||
}
|
||||
runtime.state = hydrate(runtime.config);
|
||||
// 启用 UptimeRobot 时先同步一次,保证首次渲染就有异地探测数据
|
||||
await syncUptimeRobot(runtime);
|
||||
if (externalSyncEnabled(runtime.config)) await syncUptimeRobot(runtime);
|
||||
|
||||
if (ONCE) {
|
||||
await probeDue(runtime, true);
|
||||
@@ -1024,7 +1071,7 @@ async function main() {
|
||||
})
|
||||
.catch((err) => console.error(`[tick] ${err.stack || err}`));
|
||||
// UptimeRobot 按自身 pollIntervalSec 拉取,失败不影响本地巡检
|
||||
if (Date.now() - runtime.ur.lastSyncMs >= uptimeRobotConfig(runtime.config).pollIntervalSec * 1000) {
|
||||
if (externalSyncEnabled(runtime.config) && Date.now() - runtime.ur.lastSyncMs >= uptimeRobotConfig(runtime.config).pollIntervalSec * 1000) {
|
||||
syncUptimeRobot(runtime)
|
||||
.then(() => scheduleWrite(runtime))
|
||||
.catch(() => { /* syncUptimeRobot 内部已记录错误 */ });
|
||||
@@ -1041,7 +1088,17 @@ async function main() {
|
||||
console.log(`面板: http://localhost:${PORT}/`);
|
||||
console.log(`状态 API: http://localhost:${PORT}/api/status`);
|
||||
console.log(`兼容接口: http://localhost:${PORT}/api/v2/getMonitors (UptimeRobot getMonitors 只读兼容)`);
|
||||
console.log(`监控目标: ${runtime.config.monitors.length} 个自建探测${runtime.ur.enabled ? ` + ${runtime.ur.count} 个 UptimeRobot 监控` : ''}(配置热重载已启用)`);
|
||||
const scope = probeScopeSetting(runtime.config);
|
||||
const localCount = localProbeTargets(runtime.config).length;
|
||||
const scopeText = {
|
||||
all: `全部(自建 ${localCount} 个${runtime.ur.enabled ? ` + UptimeRobot ${runtime.ur.count} 个` : ''})`,
|
||||
config: `仅自建(${localCount} 个,不同步 UptimeRobot)`,
|
||||
uptimerobot: `仅 UptimeRobot(${runtime.ur.count} 个,**不探测本地配置里的 ${runtime.config.monitors.length} 个目标**)`,
|
||||
}[scope];
|
||||
console.log(`展示口径: ${dataFilter(runtime.config)} 探测范围: ${scope} —— ${scopeText}(settings.probeScope 可显式覆盖)`);
|
||||
if (!localCount && !runtime.ur.enabled) {
|
||||
console.warn('[warn] 当前没有任何数据源:本地探测已关闭且 UptimeRobot 未启用,面板将显示「等待检测」');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user