1
0

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:
2026-05-14 12:32:41 +08:00
parent e983e5d75d
commit 1c5cfafda6
47 changed files with 1768 additions and 1231 deletions

View File

@@ -30,10 +30,9 @@ src/
routes/ API 路由 handler按端点拆分
health.ts GET /health无 store 参数)
meta.ts GET /api/meta
summary.ts GET /api/summary
targets.ts GET /api/targets
dashboard.ts GET /api/dashboard
metrics.ts GET /api/targets/:id/metrics
history.ts GET /api/targets/:id/history
trend.ts GET /api/targets/:id/trend
checker/
types.ts 基础类型定义ResolvedTargetBase、RawTargetConfig、DefaultsConfig、CheckResult 等基础 interface
config-loader.ts YAML 配置解析、契约校验、语义校验与运行期解析(输出 ResolvedConfig
@@ -73,11 +72,10 @@ src/
target-table-sorters.ts 表格排序器
color-threshold.ts 可用率颜色阈值函数
hooks/ TanStack Query 数据层
use-queries.ts 全局面板查询 hooksummary/targets/meta
use-queries.ts 全局面板查询 hookdashboard/meta/metrics
use-target-detail.ts 目标详情 Drawer 状态与条件查询 hook
utils/ 前端工具函数
time.ts 时间处理subtractHours
stats.ts 趋势统计计算computeTrendStats
time.ts 时间处理subtractHours、相对时间、动态时长单位
scripts/ 构建、schema 生成和清理脚本
tests/ Bun test 测试(结构镜像 src 目录)
openspec/ OpenSpec 变更与规格文档
@@ -142,10 +140,9 @@ routes: {
"/*": homepage, // HTML importSPA fallback
"/api/*": () => jsonResponse(createApiError("API route not found", 404), { mode, status: 404 }),
"/api/meta": { GET: () => handleMeta(mode) },
"/api/summary": { GET: () => handleSummary(store, mode) },
"/api/targets": { GET: () => handleTargets(store, mode) },
"/api/dashboard": { GET: (req) => handleDashboard(new URL(req.url), store, mode) },
"/api/targets/:id/history": { GET: (req) => handleHistory(req.params.id, new URL(req.url), store, mode) },
"/api/targets/:id/trend": { GET: (req) => handleTrend(req.params.id, new URL(req.url), store, mode) },
"/api/targets/:id/metrics": { GET: (req) => handleMetrics(req.params.id, new URL(req.url), store, mode) },
"/health": { GET: () => handleHealth(mode) },
}
```
@@ -157,20 +154,17 @@ Handler 函数签名因端点而异:
export function handleHealth(mode: RuntimeMode): Response;
export function handleMeta(mode: RuntimeMode): Response;
// 仅有 store 的路由
export function handleSummary(store: ProbeStore, mode: RuntimeMode): Response;
export function handleTargets(store: ProbeStore, mode: RuntimeMode): Response;
// 带 target ID 和查询参数的路由
export function handleDashboard(url: URL, store: ProbeStore, mode: RuntimeMode): Response;
export function handleHistory(idStr: string, url: URL, store: ProbeStore, mode: RuntimeMode): Response;
export function handleTrend(idStr: string, url: URL, store: ProbeStore, mode: RuntimeMode): Response;
export function handleMetrics(idStr: string, url: URL, store: ProbeStore, mode: RuntimeMode): Response;
```
**请求处理流程**
1. `Bun.serve``routes` 对象按路径 + HTTP 方法匹配请求
2. 未匹配方法的请求落入 `/api/*` 通配符(返回 404
3. 各 handler 内部通过 `middleware.ts` 提供的 `validateTargetId``validateTimeRange``validatePagination` 做参数校验,`pageSize` 最大值为 `200`
3. 各 handler 内部通过 `middleware.ts` 提供的 `validateTargetId``validateTimeRange``validatePagination``validateDashboardWindow``validateRecentLimit``validateMetricsBucket` 做参数校验,`pageSize` 最大值为 `200`
4. 校验函数返回 `Response` 实例表示校验失败(直接返回),返回数据对象表示通过
5. 业务逻辑通过 `store` 查询数据,用 `helpers.ts``jsonResponse``mapCheckResult``formatDuration` 等格式化输出
@@ -190,7 +184,7 @@ export function handleTrend(idStr: string, url: URL, store: ProbeStore, mode: Ru
- `formatDuration(ms)` — 毫秒转为可读时长字符串
- `jsonResponse(body, options)` — JSON 响应构造
- `mapCheckResult(row)` — 数据库行转 API CheckResult
- **`middleware.ts`**API 参数校验函数(`validateTargetId``validateTimeRange``validatePagination`,其中 `pageSize` 上限为 `200`
- **`middleware.ts`**API 参数校验函数(`validateTargetId``validateTimeRange``validatePagination``validateDashboardWindow``validateRecentLimit``validateMetricsBucket`,其中 `pageSize``recentLimit` 上限为 `200`
### 1.5 类型定义规范
@@ -428,19 +422,20 @@ TcpChecker implements Checker
**核心方法**
| 方法 | 用途 |
| ----------------------- | ---------------------------------------------------------------- |
| `syncTargets(targets)` | 启动期同步 targets基于 name 做 upsert + delete 事务) |
| `insertCheckResult()` | 写入单条检查结果 |
| `getTargets()` | 查询全部 targetsdefault 分组优先排序) |
| `getLatestChecksMap()` | 批量获取每个 target 的最新检查结果(单次 SQL 聚合) |
| `getAllTargetStats()` | 批量获取每个 target 的可用率统计GROUP BY 聚合) |
| `getAllRecentSamples()` | 批量获取每个 target 的最近 N 条采样window function |
| `getSummary()` | 获取总览统计(基于 `getLatestChecksMap` 内存计算 up/down/total |
| `getTrend()` | 获取按小时聚合的趋势数据 |
| `getHistory()` | 分页查询历史记录 |
| `getRecentSamples()` | 获取最近 N 条采样数据(用于状态条渲染) |
| `prune(retentionMs)` | 按 retention 策略清理过期数据(由 engine 定时调用) |
| 方法 | 用途 |
| ------------------------------------------ | ----------------------------------------------------------- |
| `syncTargets(targets)` | 启动期同步 targets基于 name 做 upsert + delete 事务) |
| `insertCheckResult()` | 写入单条检查结果 |
| `getTargets()` | 查询全部 targetsdefault 分组优先排序) |
| `getLatestChecksMap()` | 批量获取每个 target 的最新检查结果(单次 SQL 聚合) |
| `getAllTargetWindowStats(from, to)` | 批量获取窗口内每个 target 的 total/up/down 基础计数 |
| `getDashboardIncidentStates(from, to)` | 获取 Dashboard 窗口内状态序列,供应用层计算 incidents |
| `getAllRecentSamples(limit)` | 批量获取每个 target 的最近 N 条采样(用于状态条和连续状态 |
| `getTargetCheckpoints(targetId, from, to)` | 获取单目标窗口内检查点序列,供 metrics 应用层分桶和故障分析 |
| `getTargetDurations(targetId, from, to)` | 获取单目标窗口内成功检查耗时升序数组,供应用层计算 P95/P99 |
| `getHistory()` | 分页查询历史记录 |
| `getRecentSamples()` | 获取最近 N 条采样数据(用于状态条渲染) |
| `prune(retentionMs)` | 按 retention 策略清理过期数据(由 engine 定时调用) |
**Statement 使用规范**
@@ -453,7 +448,13 @@ TcpChecker implements Checker
- 避免 N+1 查询:批量场景优先用单次 SQL 聚合GROUP BY、子查询 JOIN+ 内存组装
- 新增批量查询方法时必须编写对应单元测试
- `getSummary()``GET /api/targets` 的响应组装通过 `getLatestChecksMap` + `getAllTargetStats` + `getAllRecentSamples` 实现批量查询
- `GET /api/dashboard` 的响应组装通过 `getLatestChecksMap` + `getAllTargetWindowStats` + `getAllRecentSamples` + `getDashboardIncidentStates` 实现批量查询
**轻数据库指标计算规范**
- 数据库只负责存储、筛选、排序、分页、LIMIT 和标准 SQL 基础聚合(如 `COUNT``SUM(CASE)``AVG``MIN``MAX``GROUP BY`),用于减少应用层输入数据量
- 指标语义必须在后端应用层实现包括可用率舍入、百分位、状态翻转、故障段识别、MTTR、最长故障、连续状态、趋势 UTC 小时分桶和窗口边界处理
- 禁止用 SQLite 专有时间函数承载趋势分桶语义,禁止用复杂 SQL/window function 承载故障事件或恢复时长等业务规则
**Schema**
@@ -553,15 +554,15 @@ main.tsx
└── ErrorBoundaryReact 错误边界)
└── QueryClientProviderTanStack Query 全局挂载)
├── App根组件
│ ├── useDashboard() ─── GET /api/dashboard?window=24h&recentLimit=308s 轮询)
│ ├── SummaryCards总览统计卡片
│ │ └── useSummary() ─── GET /api/summary8s 轮询)
│ └── TargetBoard目标列表
│ ├── useTargets() ─── GET /api/targets8s 轮询)
│ ├── useMeta() ───── GET /api/meta应用生命周期内缓存
│ ├── DashboardResponse.targets
│ ├── useMeta() ───── GET /api/meta应用生命周期内缓存
│ └── TargetGroup[](按 group 字段分组)
│ └── PrimaryTable ← createTargetTableColumns(checkerTypes)
│ └── TargetDetailDrawer目标详情抽屉
│ └── useTargetDetail() ── 按需发起 trend + history 查询
│ └── useTargetDetail() ── 按需发起 metrics + history 查询
│ ├── OverviewTab → Statistic + TrendChart + StatusDonut + Descriptions
│ └── HistoryTab → PrimaryTable分页历史记录
└── ReactQueryDevtools开发工具仅开发环境
@@ -571,14 +572,14 @@ main.tsx
```
hooks/use-queries.ts全局面板级查询
├── queryKeyssummary/targets/meta 结构化 query key
├── useSummary() → /api/summary8s 自动轮询)
├── useTargets() → /api/targets8s 自动轮询
└── useMeta() → /api/metastaleTime: Infinity
├── queryKeysdashboard/meta/metrics 结构化 query key
├── useDashboard() → /api/dashboard?window=24h&recentLimit=308s 自动轮询)
├── useTargetMetrics() → /api/targets/:id/metrics详情按需加载
└── useMeta() → /api/metastaleTime: Infinity
hooks/use-target-detail.tsDrawer 状态与详情级条件查询)
├── 内部复用 useTargets() 的缓存来查找 selectedTarget
├── useQuery(/api/targets/:id/trend)条件查询enabled 仅当 Drawer 打开且时间范围有效)
├── 内部复用 useDashboard() 的缓存来查找 selectedTarget
├── useTargetMetrics(/api/targets/:id/metrics)条件查询enabled 仅当 Drawer 打开且时间范围有效)
└── useQuery(/api/targets/:id/history)(条件查询:含分页)
```
@@ -588,10 +589,10 @@ hooks/use-target-detail.tsDrawer 状态与详情级条件查询)
```typescript
const queryKeys = {
summary: () => ["summary"] as const,
targets: () => ["targets"] as const,
dashboard: () => ["dashboard", "24h", 30] as const,
meta: () => ["meta"] as const,
trend: (targetId: number, from: string, to: string) => ["trend", targetId, from, to] as const,
metrics: (targetId: number, from: string, to: string, bucket: "1h") =>
["metrics", targetId, from, to, bucket] as const,
history: (targetId: number, from: string, to: string, page: number) => ["history", targetId, from, to, page] as const,
};
```
@@ -605,16 +606,16 @@ const queryKeys = {
```typescript
// 全局面板级查询(需要持续刷新)
useQuery({
queryKey: queryKeys.summary(),
queryFn: () => fetchJson<SummaryResponse>("/api/summary"),
queryKey: queryKeys.dashboard(),
queryFn: () => fetchJson<DashboardResponse>("/api/dashboard?window=24h&recentLimit=30"),
refetchInterval: 8000, // 自动轮询间隔
refetchIntervalInBackground: false, // 切后台不轮询
});
// 详情级查询(按需加载)
useQuery({
queryKey: selectedTargetId ? queryKeys.trend(id, from, to) : ["trend", "disabled"],
queryFn: () => fetchJson(`/api/targets/${id}/trend?...`),
queryKey: selectedTargetId ? queryKeys.metrics(id, from, to, "1h") : ["metrics", "disabled"],
queryFn: () => fetchJson(`/api/targets/${id}/metrics?...`),
enabled: selectedTargetId !== null && !!timeFrom && !!timeTo, // 条件查询
});
```
@@ -769,7 +770,7 @@ const server = Bun.serve({
routes: {
"/*": homepage, // SPA fallback开发模式自动注入 HMR
"/api/*": () => ..., // API 通配符(未匹配路由返回 404
"/api/summary": { GET: () => handleSummary(store, mode) },
"/api/dashboard": { GET: (req) => handleDashboard(new URL(req.url), store, mode) },
"/health": { GET: () => handleHealth(mode) },
// ...
},
@@ -781,9 +782,9 @@ const server = Bun.serve({
#### 路由优先级
Bun routes 的匹配规则:具体路径 > 通配符。`/api/summary` 优先于 `/api/*``/health` 优先于 `/*`
Bun routes 的匹配规则:具体路径 > 通配符。`/api/dashboard` 优先于 `/api/*``/health` 优先于 `/*`
未匹配 method 的请求(如 POST /api/summary)会落入 `/api/*` 通配符返回 404。
未匹配 method 的请求(如 POST /api/dashboard)会落入 `/api/*` 通配符返回 404。
### 3.3 构建打包