feat: P1 收尾 —— 事件面板区块与 90 天响应时间趋势
事件时间线(面板侧):
- 监控列表下方「最近事件」区块(最近 20 条:时间/类型/目标/详情),
无事件自动隐藏;维护窗口内事件带「维护」标记;窄屏隐藏详情列
响应时间趋势(P1-2):
- 当日分桶在 {total,up} 外累计 latSum/latCount/latMax(仅在线样本)
- 快照输出 lat: { avg, max, days:[{key,avg}] }
- 卡片视图:可用率条下方新增「90 天日均延迟」细柱图(最近 30 天 + 均值)
- 表格视图:可用率列 tooltip 追加「90 天日均延迟 X ms(峰值 Y ms)」
- --demo 同步生成 90 天延迟样例(含起伏),静态部署也能看到效果
验证:verify-views 新增 [J] 段 5 条断言(趋势条/30 格/tooltip/事件空态/无溢出),
六套合计 159 项全通过(17+35+26+20+39+22)。
至此 P0(通知、证书)与 P1(事件时间线、维护窗口、响应时间趋势)全部落地。
This commit is contained in:
+41
-6
@@ -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.10.0';
|
||||
const VERSION = '0.11.0';
|
||||
const UR_DEFAULT_POLL_SEC = 300;
|
||||
const UR_MIN_REFRESH_GAP_MS = 60000;
|
||||
|
||||
@@ -118,14 +118,43 @@ function pruneDaily(daily) {
|
||||
while (keys.length > UPTIME_DAYS) delete daily[keys.shift()];
|
||||
}
|
||||
|
||||
function recordDaily(daily, ok) {
|
||||
// 记录一次探测到「当日分桶」;latency 仅在在线时传入(离线结果没有有意义的延迟)
|
||||
// 同时累计延迟统计(sum/count/max),用于面板的 90 天延迟趋势
|
||||
function recordDaily(daily, ok, latency = null) {
|
||||
const key = dayKey();
|
||||
const bucket = daily[key] ?? (daily[key] = { total: 0, up: 0 });
|
||||
bucket.total++;
|
||||
if (ok) bucket.up++;
|
||||
if (ok) {
|
||||
bucket.up++;
|
||||
if (Number.isFinite(latency) && latency >= 0) {
|
||||
bucket.latSum = (bucket.latSum ?? 0) + latency;
|
||||
bucket.latCount = (bucket.latCount ?? 0) + 1;
|
||||
bucket.latMax = Math.max(bucket.latMax ?? 0, latency);
|
||||
}
|
||||
}
|
||||
pruneDaily(daily);
|
||||
}
|
||||
|
||||
// 从分桶聚合延迟趋势(90 天):返回 { avg, max, days: [{ key, avg }] }
|
||||
function latencyTrend(daily) {
|
||||
const keys = Object.keys(daily ?? {}).sort();
|
||||
const days = [];
|
||||
let sum = 0;
|
||||
let count = 0;
|
||||
let max = 0;
|
||||
for (const key of keys) {
|
||||
const b = daily[key];
|
||||
if (!b?.latCount) continue;
|
||||
const avg = Math.round(b.latSum / b.latCount);
|
||||
days.push({ key, avg });
|
||||
sum += b.latSum;
|
||||
count += b.latCount;
|
||||
max = Math.max(max, b.latMax ?? 0);
|
||||
}
|
||||
if (!count) return null;
|
||||
return { avg: Math.round(sum / count), max, days };
|
||||
}
|
||||
|
||||
function intervalMs(monitor) {
|
||||
const sec = Number(monitor.interval);
|
||||
// 非法或缺失的 interval 一律回落到默认值,避免算出 NaN 让该目标永久不再被调度
|
||||
@@ -180,7 +209,7 @@ async function syncUptimeRobot(runtime, { minGapMs = 0 } = {}) {
|
||||
s.history = item.history;
|
||||
s.uptimeRatios = item.uptimeRatios;
|
||||
s.downSeconds = item.downSeconds;
|
||||
if (item.status !== 'pending') recordDaily(s.daily, item.status === 'up');
|
||||
if (item.status !== 'pending') recordDaily(s.daily, item.status === 'up', item.latency);
|
||||
}
|
||||
// 清理 UR 侧已删除的监控
|
||||
for (const [id, s] of runtime.state) {
|
||||
@@ -287,7 +316,7 @@ async function probeDue(runtime, force = false) {
|
||||
s.lastResult = result;
|
||||
s.history.push({ t: result.checkedAt, ok: result.status === 'up', ms: result.latency ?? 0 });
|
||||
if (s.history.length > HISTORY_LIMIT) s.history.shift();
|
||||
recordDaily(s.daily, result.status === 'up');
|
||||
recordDaily(s.daily, result.status === 'up', result.latency);
|
||||
const tag = result.status === 'up' ? '[up] ' : '[down]';
|
||||
console.log(`${tag} ${m.id.padEnd(20)} ${String(result.latency ?? '-').padStart(5)}ms ${result.detail}`);
|
||||
} catch (err) {
|
||||
@@ -633,6 +662,9 @@ function snapshot(runtime, demo = false) {
|
||||
const maintenance = activeMaintenanceFor(config, id);
|
||||
if (maintenance) entry.maintenance = { note: maintenance.note ?? null, to: maintenance.to ?? null };
|
||||
}
|
||||
// 90 天延迟趋势(自当日分桶聚合,自建与 UR 镜像目标都适用)
|
||||
const trend = latencyTrend(s.daily);
|
||||
if (trend) entry.lat = trend;
|
||||
monitors[id] = entry;
|
||||
}
|
||||
|
||||
@@ -762,7 +794,10 @@ function writeDemo(config) {
|
||||
const key = dayKey(new Date(Date.now() - d * 86400000));
|
||||
const missRatio = alwaysDown ? 1 : (seed + d * 13) % 23 === 0 ? 0.05 : (seed + d * 7) % 11 === 0 ? 0.012 : 0;
|
||||
const miss = Math.round(perDay * missRatio);
|
||||
daily[key] = { total: perDay, up: perDay - miss };
|
||||
// 演示用延迟:随日期缓慢起伏,让 90 天趋势有形状可看
|
||||
const up = perDay - miss;
|
||||
const avg = alwaysDown ? 0 : 40 + ((seed + d * 5) % 160);
|
||||
daily[key] = { total: perDay, up, latSum: avg * up, latCount: up, latMax: avg + 60 };
|
||||
}
|
||||
s.daily = daily;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user