1
0
Files
DiAL/src/server/routes/dashboard.ts
lanyuanxiaoyao 1c5cfafda6 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 并归档变更
2026-05-14 12:32:41 +08:00

101 lines
3.3 KiB
TypeScript

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;
}