# 审计 — 子机离线告警 + 仪表盘统计/舰队趋势（`7209f53`）

**Changelog**:
- `docs/changelog/2026-06-08-server-offline-alert.md`
- `docs/changelog/2026-06-09-dashboard-stat-cards-align.md`
- `docs/changelog/2026-06-09-fleet-trend-alerts.md`
- `docs/changelog/2026-06-09-fleet-trend-interaction.md`
- `docs/changelog/2026-06-09-offline-count-accuracy.md`

**部署验证**: `docs/reports/2026-06-09-server-offline-alert-deploy-verification.md`

## 审计信息

- **日期**: 2026-06-09
- **审计人**: Cursor Agent
- **触发原因**: 新功能（离线 Telegram 告警、设置开关）+ 仪表盘统计/趋势增强；已部署生产，补全门控审计

## 审计范围

| 文件 | 行数 | 状态 |
|------|------|------|
| `server/background/server_offline_monitor.py` | 88 | ☑ 已审 |
| `server/api/websocket.py` | +37 | ☑ 已审 |
| `server/infrastructure/telegram/__init__.py` | +37 | ☑ 已审 |
| `server/utils/notify_toggles.py` | +5 | ☑ 已审 |
| `server/config.py` | +2 | ☑ 已审 |
| `server/api/settings.py` | +1 | ☑ 已审 |
| `server/main.py` | +6 | ☑ 已审 |
| `server/application/server_connectivity.py` | +7 | ☑ 已审 |
| `server/utils/fleet_alert_buckets.py` | 88 | ☑ 已审 |
| `server/api/servers.py` | +18 | ☑ 已审 |
| `frontend/src/types/api.ts` | +1 | ☑ 已审 |
| `frontend/src/composables/useServerStatsCards.ts` | 76 | ☑ 已审 |
| `frontend/src/components/StatCardsRow.vue` | 136 | ☑ 已审 |
| `frontend/src/pages/DashboardPage.vue` | Δ | ☑ 已审 |
| `frontend/src/pages/ServersPage.vue` | Δ | ☑ 已审 |
| `frontend/src/components/dashboard/DashboardFleetTrends.vue` | 363 | ☑ 已审 |
| `frontend/src/components/dashboard/FleetTrendEChart.vue` | 新 | ☑ 已审 |
| `frontend/src/components/dashboard/FleetTrendKpiStrip.vue` | 新 | ☑ 已审 |
| `frontend/src/utils/fleetTrendAnalytics.ts` | 新 | ☑ 已审 |
| `frontend/package.json` / `package-lock.json` | echarts 依赖 | ☑ 已审 |
| `tests/test_server_offline_monitor.py` | 85 | ☑ 已审 |
| `tests/test_fleet_alert_buckets.py` | 新 | ☑ 已审 |

## 审计8步结果

### Step 1: 登记 ✅

提交 `7209f53`，范围见上表；设计/计划文档齐全。

### Step 2: 全文 Read ✅

上述文件均已通读；无跨层直连（monitor → websocket → telegram；fleet-metrics → repo）。

### Step 3: 规则扫描 H

| ID | 规则 | 命中点 | 初判 |
|----|------|--------|------|
| H1 | 无明文密钥 | config/settings | SAFE |
| H2 | 无静默吞错 | `server_offline_monitor` L86-87 `exc_info=True` | SAFE |
| H3 | Telegram HTML 转义 | `send_telegram_offline_alert` `html.escape` name/hb | SAFE |
| H4 | 通知开关门控 | `offline_alert_notify_enabled` + settings `MUTABLE_KEYS` | SAFE |
| H5 | 边沿触发防轰炸 | `_prev_online` + `_should_push_telegram` 5min | SAFE |
| H6 | 冷启动不误报 | `prev_online is None` 只记录 | SAFE |
| H7 | 仅 primary worker | `main.py` 与 self_monitor 同锁 | SAFE |
| H8 | API 鉴权 | `/fleet-metrics` `get_current_admin` | SAFE |
| H9 | CUD 审计 | `notify_alert_offline` 走既有 PUT settings + audit | SAFE |
| H10 | 离线判定一致性 | `heartbeat_indicates_online` 与 stats/list 共用 | SAFE |
| H11 | 前端 XSS | 统计数字字符串化；ECharts 无 `v-html` 用户输入 | SAFE |
| H12 | 依赖体积 | echarts ~625kB chunk | OBS（已知，懒加载页内） |

### Step 4: Closure表

| ID | 判定 | 依据 |
|----|------|------|
| H1 | SAFE | 无新增密钥字段 |
| H2 | SAFE | tick 失败打 error 日志，不掩盖 |
| H3 | SAFE | 与现有 `send_telegram_alert` 一致转义 |
| H4 | SAFE | 关开关不推 Telegram；`alert_logs`/WS 仍记录（与资源告警策略一致） |
| H5 | SAFE | 持续离线不重复；恢复后再离线可再推 |
| H6 | SAFE | 单测 `test_cold_start_no_alert` |
| H7 | SAFE | `_background_tasks` 仅 primary 注册 |
| H8 | SAFE | 管理员 JWT |
| H9 | SAFE | `MUTABLE_KEYS` 含 `notify_alert_offline` |
| H10 | SAFE | `test_server_connectivity` |
| H11 | SAFE | Vue 文本绑定 |
| H12 | ACCEPT | 仪表盘页专用 chunk，可后续 code-split 优化 |

### Step 5: 入口表

| 入口 | 鉴权 | 说明 |
|------|------|------|
| 后台 `server_offline_monitor_loop` | primary worker | Redis 读 + DB 读 |
| `broadcast_offline_alert` | 内部调用 | WS + alert_logs + Telegram |
| `PUT /api/settings/notify_alert_offline` | admin JWT | 开关 |
| `GET /api/servers/fleet-metrics` | admin JWT | 趋势+告警挂载 |
| `GET /api/servers/stats` | admin JWT | 统计卡（既有） |
| 设置页 `NOTIFY_TOGGLE_ITEMS` | 前端 | 展示开关 |

### Step 6: 输入 → Sink

| 路径 | Sink | 防护 |
|------|------|------|
| Redis `heartbeat:*` | 离线判定 | 只读；server_id 来自 DB 整数主键 |
| `server_name` / `last_heartbeat` | Telegram HTML | `html.escape` |
| `hours` query | fleet-metrics | `max(1,min(hours,168))` |
| settings value | DB settings | 布尔字符串白名单流程 |

### Step 7: 归类

```
server_offline_monitor.py     88行  12H  0FINDING
websocket.py (offline块)      37行  12H  0FINDING
telegram/__init__.py          37行  12H  0FINDING
fleet_alert_buckets.py        88行  12H  0FINDING
servers.py (fleet-metrics)    18行  12H  0FINDING
frontend (统计+趋势)           —    12H  0FINDING
tests                         —    12H  0FINDING
──────────────────────────────────────────────
总计                          12H   0FINDING
```

### Step 8: DoD ✅

- [x] 设计文档 + 实施说明 + changelog
- [x] 单测：`test_server_offline_monitor` 4、`test_fleet_alert_buckets`、`test_server_connectivity`、`test_fleet_metrics`
- [x] `pytest` 相关 51 项通过（offline+fleet+notify）
- [x] 门控 pre_deploy 7/7（部署前）
- [x] 生产部署 `/health` ok、`/app/` 200（`7209f53`）
- [ ] 浏览器终验：设置页「子机离线告警」开关 + 实机离线 Telegram（运维操作）

## FINDING 列表

无（0 FINDING）

## OBSERVATION（非阻断）

1. **离线发现延迟**：与 UI 一致，依赖 Redis `heartbeat:*` TTL（`FLUSH_INTERVAL×1.5` ≈ 15min）或键缺失；monitor 30s 轮询但状态变更是 TTL 驱动。设计文档已说明，与既有在线判定同源。
2. **舰队趋势 tooltip**：`fleet_alert_buckets.ALERT_TYPE_LABELS` 未含 `offline`，趋势图钉显示原始 `offline` 而非「离线」——纯展示，可后续 1 行补齐。
3. **`_prev_online` 内存**：删除子机后字典条目残留，量级可忽略；primary 重启会冷启动。

## 验证命令

```bash
pytest tests/test_server_offline_monitor.py tests/test_fleet_alert_buckets.py \
  tests/test_server_connectivity.py tests/test_fleet_metrics.py \
  tests/test_notify_toggle_sync.py -q
# 51 passed
```

## 结论

☑ **审计通过，0 FINDING**，已部署生产；待用户浏览器/实机离线 Telegram 终验。
