## Purpose

定义拨测系统的 REST API 端点：Dashboard 聚合 API、单目标指标 API、带时间范围和分页的历史记录、共享类型定义和 API 错误处理。

## Requirements

### Requirement: Dashboard 聚合 API
系统 SHALL 提供 `GET /api/dashboard` 端点，返回 Dashboard 首屏所需的总览统计和目标列表数据。targets 列表 SHALL 仅包含活跃目标。

#### Scenario: 获取 Dashboard 数据
- **WHEN** 客户端请求 `GET /api/dashboard?window=24h&recentLimit=30`
- **THEN** 系统 SHALL 返回 JSON 包含 summary 和 targets 字段，targets 仅包含 active=1 的目标

#### Scenario: summary 字段
- **WHEN** Dashboard 响应包含 summary
- **THEN** summary SHALL 仅统计活跃目标：total（活跃目标数）、up（活跃正常目标数）、down（活跃异常目标数）、lastCheckTime（最近一次检查时间）、incidents（活跃目标在指定窗口内异常事件数）、window（from/to/label）字段

#### Scenario: targets 字段
- **WHEN** Dashboard 响应包含 targets
- **THEN** targets 数组中每个元素 SHALL 为活跃目标，包含目标基本信息（id、name、description、group、type、target、interval）、latestCheck、stats、currentStreak 和 recentSamples 字段，其中 name 和 description 均为 null 或字符串

#### Scenario: target name 字段为 null
- **WHEN** 某个 target 未配置 `name` 或显式配置 `name: null`
- **THEN** Dashboard targets 响应中对应元素 SHALL 返回 `name: null`

#### Scenario: target description 字段
- **WHEN** 某个 target 配置了 `description`
- **THEN** Dashboard targets 响应中对应元素 SHALL 返回该 description 值

#### Scenario: window 参数缺失
- **WHEN** 客户端请求 `GET /api/dashboard` 未提供 window 参数
- **THEN** 系统 SHALL 默认使用 window=`24h`

#### Scenario: window 参数语义
- **WHEN** 系统处理 Dashboard 请求
- **THEN** 系统 SHALL 以服务端当前时间作为 window.to，以 window 参数换算 window.from，并在响应中回显 window.from、window.to、window.label

#### Scenario: window 参数有效值
- **WHEN** 客户端请求 Dashboard 端点并指定 window 参数
- **THEN** 系统 SHALL 接受 `24h` 作为有效值；其他值 SHALL 返回 400 状态码和错误信息

#### Scenario: 不支持的 window 参数
- **WHEN** 客户端请求 `GET /api/dashboard?window=abc`
- **THEN** 系统 SHALL 返回 400 状态码和错误信息

#### Scenario: recentLimit 参数缺失
- **WHEN** 客户端请求 `GET /api/dashboard?window=24h` 未提供 recentLimit 参数
- **THEN** 系统 SHALL 默认使用 recentLimit=30

#### Scenario: 不支持的 recentLimit 参数
- **WHEN** 客户端请求 `GET /api/dashboard?recentLimit=0` 或超过系统上限的 recentLimit
- **THEN** 系统 SHALL 返回 400 状态码和错误信息

#### Scenario: 目标无历史记录
- **WHEN** 某目标尚未执行过任何拨测
- **THEN** 其 latestCheck 为 null，recentSamples 为空数组，stats.totalChecks 为 0，stats.availability 为 0，currentStreak 为 null

### Requirement: Dashboard 指标字段
Dashboard API SHALL 返回基于时间窗口计算的目标统计和连续状态字段。

#### Scenario: 目标 stats 字段
- **WHEN** Dashboard 响应包含目标 stats
- **THEN** stats SHALL 包含 totalChecks、upChecks、downChecks、availability 字段，且这些字段 SHALL 基于请求 window 对应的时间范围计算

#### Scenario: 目标 currentStreak 字段
- **WHEN** Dashboard 响应包含目标 currentStreak
- **THEN** currentStreak SHALL 为 `{ up: boolean, count: number, capped?: boolean }` 或 null

#### Scenario: currentStreak 达到 recentLimit
- **WHEN** 连续状态次数达到 recentLimit 上限
- **THEN** currentStreak.capped SHALL 为 true

#### Scenario: recentSamples 字段
- **WHEN** Dashboard 响应包含 recentSamples
- **THEN** 每个 recentSamples 元素 SHALL 包含 timestamp、durationMs、up 字段，其中 up 为 boolean 且等于 matched

### Requirement: 单目标指标 API
系统 SHALL 提供 `GET /api/targets/:id/metrics` 端点，返回 Drawer 概览所需的单目标统计和趋势数据。仅活跃目标的指标 SHALL 可查询。端点的详细计算规则（P95/P99、MTTR、故障分析、趋势分桶等）定义在 `target-metrics-api` 能力中。

#### Scenario: 指定时间范围查询指标
- **WHEN** 客户端请求 `GET /api/targets/1/metrics?from=ISO&to=ISO&bucket=1h` 且该目标为活跃目标
- **THEN** 系统 SHALL 返回 targetId、window、stats、trend 字段

#### Scenario: from 或 to 参数缺失
- **WHEN** 客户端请求 `GET /api/targets/1/metrics` 未提供 from 或 to 参数
- **THEN** 系统 SHALL 返回 400 状态码和错误信息

#### Scenario: 目标不存在或非活跃
- **WHEN** 客户端请求 `GET /api/targets/999/metrics?from=ISO&to=ISO` 且该目标不存在或 active=0
- **THEN** 系统 SHALL 返回 404 状态码和错误信息

#### Scenario: 无效的目标 ID
- **WHEN** 客户端请求 `GET /api/targets/abc/metrics?from=ISO&to=ISO`
- **THEN** 系统 SHALL 返回 400 状态码和错误信息

#### Scenario: bucket 参数缺失
- **WHEN** 客户端请求 `GET /api/targets/1/metrics?from=ISO&to=ISO` 未提供 bucket 参数
- **THEN** 系统 SHALL 默认使用 bucket=`1h`

#### Scenario: 不支持的 bucket 参数
- **WHEN** 客户端请求 `GET /api/targets/1/metrics?from=ISO&to=ISO&bucket=5m`
- **THEN** 系统 SHALL 返回 400 状态码和错误信息

### Requirement: 历史记录 API
系统 SHALL 保留 `GET /api/targets/:id/history` 端点，支持时间范围筛选和分页返回指定目标的拨测记录。仅活跃目标的历史记录 SHALL 可查询。

#### Scenario: 获取指定时间范围内的历史记录
- **WHEN** 客户端请求 `GET /api/targets/1/history?from=ISO&to=ISO&page=1&pageSize=20` 且该目标为活跃目标
- **THEN** 系统 SHALL 返回带分页信息的历史记录，包含 items、total、page、pageSize，按时间倒序排列

#### Scenario: 使用默认分页参数
- **WHEN** 客户端请求 `GET /api/targets/1/history?from=ISO&to=ISO`（未指定 page 或 pageSize）
- **THEN** 系统 SHALL 使用默认 page=1, pageSize=20

#### Scenario: from 或 to 参数缺失
- **WHEN** 客户端请求 `GET /api/targets/1/history` 未提供 from 或 to 参数
- **THEN** 系统 SHALL 返回 400 状态码和错误信息

#### Scenario: 目标不存在或非活跃
- **WHEN** 客户端请求 `GET /api/targets/999/history?from=ISO&to=ISO` 且该目标不存在或 active=0
- **THEN** 系统 SHALL 返回 404 状态码和错误信息

### Requirement: 新增共享类型
系统 SHALL 在 `src/shared/api.ts` 中定义 Dashboard 和 Metrics 相关共享类型。CheckResult SHALL 包含 durationMs（null | number）、failure（CheckFailure | null）、matched（boolean）、detail（null | string）、observation（Record<string, unknown> | null）、timestamp（string）。其中 detail 替代原 statusDetail 字段名。

#### Scenario: DashboardResponse 类型
- **WHEN** 前后端共享 `DashboardResponse` 类型
- **THEN** 该类型 SHALL 包含 summary 和 targets 字段

#### Scenario: TargetStatus 类型
- **WHEN** 前后端共享 `TargetStatus` 类型
- **THEN** 该类型 SHALL 包含目标基本信息字段（id、name、description、group、type、target、interval）、stats（totalChecks、upChecks、downChecks、availability）、currentStreak 和 recentSamples 字段，其中 name 和 description 类型均为 null 或字符串

#### Scenario: TargetMetricsResponse 类型
- **WHEN** 前后端共享 `TargetMetricsResponse` 类型
- **THEN** 该类型 SHALL 包含 targetId、window、stats 和 trend 字段

#### Scenario: TrendPoint 类型
- **WHEN** 前后端共享 `TrendPoint` 类型
- **THEN** 该类型 SHALL 包含 bucketStart、avgDurationMs、minDurationMs、maxDurationMs、availability、totalChecks、upChecks、downChecks 字段

#### Scenario: CheckResult 类型变更
- **WHEN** 前端或后端引用 CheckResult 类型
- **THEN** 该类型 SHALL 包含 `timestamp: string`、`matched: boolean`、`durationMs: number | null`、`detail: string | null`、`observation: Record<string, unknown> | null`、`failure` 字段，不包含 statusDetail 字段，不包含 success 字段

#### Scenario: RecentSample 类型
- **WHEN** 前后端共享 `RecentSample` 类型
- **THEN** 该类型 SHALL 包含 `timestamp: string`、`durationMs: number | null`、`up: boolean` 字段，其中 up 为 boolean 且等于 matched

#### Scenario: HistoryResponse 类型
- **WHEN** 前后端共享 `HistoryResponse` 类型
- **THEN** 该类型 SHALL 包含 `items: CheckResult[]`、`total: number`、`page: number`、`pageSize: number` 字段

#### Scenario: API 序列化构造 detail
- **WHEN** API 路由序列化 StoredCheckResult 为 API 响应
- **THEN** 系统 SHALL 从 StoredCheckResult 中反序列化 observation，根据 target type 通过 checkerRegistry 获取对应 checker 并调用 buildDetail(observation) 动态生成 detail 字段

#### Scenario: mapCheckResult 接收 type 参数
- **WHEN** 序列化辅助函数 mapCheckResult 被调用
- **THEN** 函数 SHALL 接收 target type 参数，用于从 registry 获取对应 checker 调用 buildDetail

#### Scenario: Dashboard API 传递 type
- **WHEN** Dashboard 路由序列化 latestCheck
- **THEN** 路由 SHALL 将 target.type 传递给 mapCheckResult

#### Scenario: History API 传递 type
- **WHEN** History 路由序列化历史记录列表
- **THEN** 路由 SHALL 将已查询的 target.type 传递给 mapCheckResult

### Requirement: 保留健康检查端点
系统 SHALL 保留 `GET /health` 端点，不受拨测功能影响。

#### Scenario: 访问健康检查
- **WHEN** 客户端请求 `GET /health`
- **THEN** 系统 SHALL 返回与之前格式一致的健康检查响应

### Requirement: API 错误处理
系统 SHALL 对不存在的目标 ID、非活跃目标、无效参数和超出范围的分页参数返回适当的 HTTP 错误响应。非活跃目标与不存在的目标 SHALL 返回相同的 404 响应。

#### Scenario: 查询不存在的目标
- **WHEN** 客户端请求 `GET /api/targets/999/metrics?from=ISO&to=ISO`
- **THEN** 系统 SHALL 返回 404 状态码和错误信息

#### Scenario: 查询非活跃目标
- **WHEN** 客户端请求 `GET /api/targets/<id>/metrics?from=ISO&to=ISO` 且该目标 active=0
- **THEN** 系统 SHALL 返回 404 状态码和错误信息

#### Scenario: 无效的 from/to 参数
- **WHEN** 客户端请求 `GET /api/targets/1/metrics?from=invalid&to=ISO`
- **THEN** 系统 SHALL 返回 400 状态码和错误信息

#### Scenario: from 晚于 to
- **WHEN** 客户端请求 `GET /api/targets/1/metrics?from=<较晚时间>&to=<较早时间>`
- **THEN** 系统 SHALL 返回 400 状态码和错误信息，提示 from 必须早于 to

#### Scenario: 无效的分页参数
- **WHEN** 客户端请求 `GET /api/targets/1/history?from=ISO&to=ISO&page=abc`
- **THEN** 系统 SHALL 返回 400 状态码和错误信息

#### Scenario: pageSize 超过上限
- **WHEN** 客户端请求 `GET /api/targets/1/history?from=ISO&to=ISO&pageSize=201`
- **THEN** 系统 SHALL 返回 400 状态码和错误信息，提示 pageSize 不能超过 200

#### Scenario: pageSize 等于上限
- **WHEN** 客户端请求 `GET /api/targets/1/history?from=ISO&to=ISO&pageSize=200`
- **THEN** 系统 SHALL 正常返回数据

#### Scenario: 无效的目标 ID
- **WHEN** 客户端请求 `GET /api/targets/abc/metrics?from=ISO&to=ISO`
- **THEN** 系统 SHALL 返回 400 状态码和错误信息

### Requirement: 失败信息 API 契约
系统 SHALL 通过 API 返回结构化 failure 信息，供 Dashboard 展示和后续排查。

#### Scenario: 返回 expect 不匹配信息
- **WHEN** 最近一次检查结果包含 failure.kind=`mismatch`
- **THEN** `/api/targets` 和 `/api/targets/:id/history` SHALL 返回该 failure 的 kind、phase、path、expected、actual、message 字段

#### Scenario: 无失败信息
- **WHEN** 检查结果 matched=true
- **THEN** API SHALL 返回 failure 为 null

### Requirement: Meta 信息 API
系统 SHALL 提供 `GET /api/meta` 端点，返回系统运行时元数据。未匹配 method SHALL 按 API 通配符处理为 JSON 404，不再保留自定义 HEAD/405 语义。

#### Scenario: 获取 checker 类型列表
- **WHEN** 客户端请求 `GET /api/meta`
- **THEN** 系统 SHALL 返回 JSON `{ checkerTypes: string[] }`，包含所有已注册的 checker 类型标识符

#### Scenario: 类型列表来源
- **WHEN** 系统启动并注册了 checker
- **THEN** `/api/meta` 返回的 `checkerTypes` SHALL 与 `CheckerRegistry.supportedTypes` 完全一致

#### Scenario: 不支持的 method 请求
- **WHEN** 客户端使用 POST/PUT/DELETE/HEAD 等未声明 method 请求 `/api/meta`
- **THEN** `/api/*` 通配符 SHALL 返回 JSON 404 响应

### Requirement: MetaResponse 共享类型
系统 SHALL 在 `src/shared/api.ts` 中定义 `MetaResponse` 类型。

#### Scenario: MetaResponse 类型定义
- **WHEN** 前后端引用 `MetaResponse` 类型
- **THEN** 该类型 SHALL 包含 `checkerTypes: string[]` 字段
