- 新增 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 并归档变更
101 lines
3.3 KiB
TypeScript
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;
|
|
}
|