feat: 前端指标体系增强 — Dashboard/Metrics API、2×4 统计区、趋势图面积+异常标记、连续状态列
- 新增 GET /api/dashboard 合并原 summary+targets 首屏接口 - 新增 GET /api/targets/:id/metrics 合并原 stats+trend 概览接口 - 后端指标纯函数:可用率、百分位、故障段分析、连续状态、UTC 小时分桶 - ProbeStore 窗口取数方法替代全量历史查询 - SummaryCards 扩展为 4 卡片(新增异常事件数)+ 数据新鲜度展示 - 表格新增「连续」列(Tag 渲染 capped 状态) - OverviewTab 重构为 2×4 Statistic 多维度布局 - TrendChart 改为延迟范围面积图 + 红色异常标记点 - 删除旧路由(summary/targets/trend)和 computeTrendStats - 同步 delta specs 到主 specs 并归档变更
This commit is contained in:
100
src/server/routes/dashboard.ts
Normal file
100
src/server/routes/dashboard.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import type { DashboardResponse, RuntimeMode } from "../../shared/api";
|
||||
import type { ProbeStore } from "../checker/store";
|
||||
|
||||
import { formatDuration, jsonResponse, mapCheckResult } from "../helpers";
|
||||
import { analyzeIncidentSequence, calculateCurrentStreak, type MetricCheckpoint } from "../metrics";
|
||||
import { validateDashboardWindow, validateRecentLimit } from "../middleware";
|
||||
|
||||
export function handleDashboard(url: URL, store: ProbeStore, mode: RuntimeMode): Response {
|
||||
const windowResult = validateDashboardWindow(url.searchParams.get("window"), mode);
|
||||
if (windowResult instanceof Response) return windowResult;
|
||||
|
||||
const limitResult = validateRecentLimit(url.searchParams.get("recentLimit"), mode);
|
||||
if (limitResult instanceof Response) return limitResult;
|
||||
|
||||
const targets = store.getTargets();
|
||||
const latestChecksMap = store.getLatestChecksMap();
|
||||
const windowStats = store.getAllTargetWindowStats(windowResult.from, windowResult.to);
|
||||
const recentSamplesMap = store.getAllRecentSamples(limitResult.recentLimit);
|
||||
const incidentStates = groupDashboardIncidentStates(
|
||||
store.getDashboardIncidentStates(windowResult.from, windowResult.to),
|
||||
);
|
||||
|
||||
let up = 0;
|
||||
let down = 0;
|
||||
let lastCheckTime: null | string = null;
|
||||
let incidents = 0;
|
||||
|
||||
const responseTargets: DashboardResponse["targets"] = targets.map((target) => {
|
||||
const latest = latestChecksMap.get(target.id) ?? null;
|
||||
const stats = windowStats.get(target.id) ?? { availability: 0, downChecks: 0, totalChecks: 0, upChecks: 0 };
|
||||
const recentSamples = recentSamplesMap.get(target.id) ?? [];
|
||||
const currentStreak = calculateCurrentStreak(
|
||||
recentSamples.map((sample) => ({
|
||||
durationMs: sample.duration_ms,
|
||||
matched: sample.matched === 1,
|
||||
timestamp: sample.timestamp,
|
||||
})),
|
||||
limitResult.recentLimit,
|
||||
);
|
||||
|
||||
if (latest?.matched === 1) {
|
||||
up++;
|
||||
} else {
|
||||
down++;
|
||||
}
|
||||
|
||||
if (latest && (!lastCheckTime || latest.timestamp > lastCheckTime)) {
|
||||
lastCheckTime = latest.timestamp;
|
||||
}
|
||||
|
||||
incidents += analyzeIncidentSequence(
|
||||
incidentStates.get(target.id) ?? [],
|
||||
windowResult.from,
|
||||
windowResult.to,
|
||||
).incidentCount;
|
||||
|
||||
return {
|
||||
currentStreak,
|
||||
group: target.grp,
|
||||
id: target.id,
|
||||
interval: formatDuration(target.interval_ms),
|
||||
latestCheck: latest ? mapCheckResult(latest) : null,
|
||||
name: target.name,
|
||||
recentSamples: recentSamples.map((sample) => ({
|
||||
durationMs: sample.duration_ms,
|
||||
timestamp: sample.timestamp,
|
||||
up: sample.matched === 1,
|
||||
})),
|
||||
stats,
|
||||
target: target.target,
|
||||
type: target.type,
|
||||
};
|
||||
});
|
||||
|
||||
const response: DashboardResponse = {
|
||||
summary: {
|
||||
down,
|
||||
incidents,
|
||||
lastCheckTime,
|
||||
total: targets.length,
|
||||
up,
|
||||
window: windowResult,
|
||||
},
|
||||
targets: responseTargets,
|
||||
};
|
||||
|
||||
return jsonResponse(response, { mode });
|
||||
}
|
||||
|
||||
function groupDashboardIncidentStates(
|
||||
states: Array<{ matched: number; target_id: number; timestamp: string }>,
|
||||
): Map<number, MetricCheckpoint[]> {
|
||||
const result = new Map<number, MetricCheckpoint[]>();
|
||||
for (const state of states) {
|
||||
const list = result.get(state.target_id) ?? [];
|
||||
list.push({ durationMs: null, matched: state.matched === 1, timestamp: state.timestamp });
|
||||
result.set(state.target_id, list);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
Reference in New Issue
Block a user