# 审计 — 监测槽开关 / 8h·24h / 暂停占槽 / 探针中文错误

**日期**：2026-06-11  
**Changelog**:
- `docs/changelog/2026-06-11-watch-slot-monitoring-toggle.md`
- `docs/changelog/2026-06-11-watch-probe-parse-error-fix.md`

## 范围（未提交工作区，+799 / −124 行）

| 层级 | 文件 |
|------|------|
| 模型/迁移 | `server/domain/models/__init__.py`（`monitoring_enabled`、`ttl_hours`） |
| | `server/infrastructure/database/migrations.py` |
| Repository | `server/infrastructure/database/watch_repo.py` |
| Service | `server/application/services/watch_service.py` |
| API | `server/api/watch.py`（PATCH monitoring / ttl） |
| 探针 | `server/background/watch_probe_runner.py` |
| | `server/infrastructure/ssh/remote_probe.py` |
| 工具 | `server/utils/watch_metrics.py` |
| | `server/utils/watch_state.py`（**新增** Redis 清理） |
| | `server/utils/watch_probe_errors.py`（**新增** 中文错误） |
| 前端 | `frontend/src/composables/useWatchPins.ts` |
| | `frontend/src/components/watch/WatchSlotCard.vue` |
| | `frontend/src/components/watch/WatchSlotRow.vue` |
| | `frontend/src/components/watch/WatchProbeRecordsTable.vue` |
| | `frontend/src/utils/watchFormat.ts` |
| | `frontend/src/api/index.ts`（`http.patch`） |
| 测试 | `tests/test_watch_pins.py` |
| | `tests/test_watch_metrics.py` |
| | `tests/test_watch_probe_errors.py`（**新增**） |

## 功能摘要

1. **实时监测开关**：`monitoring_enabled`；关则停 5s 探针，槽位保留
2. **8h / 24h**：`ttl_hours` + `PATCH /pins/{slot}/ttl`；默认 8h，显式切 24h
3. **暂停永久占槽**：`monitoring_enabled=false` 时不参与 TTL 过期删除
4. **时长到期**：自动暂停（等同关开关），结束 session，**不删 pin**
5. **恢复 Agent 60s**：无活跃监测时清理 `watch:live/proc/last`；Agent 心跳逻辑未改
6. **parse_error 修复**：Agent 有 CPU/内存时 SSH 失败不拖垮整轮；SSH 脚本加固
7. **错误中文**：后端写入 + 前端 `formatProbeError` 兼容历史英文

## Step 3 规则扫描

| H | 规则 | 结论 |
|---|------|------|
| H1 | 鉴权 / IDOR | **PASS** — 新 PATCH 均 `get_current_admin`；repo 带 `admin_id` |
| H2 | 无静默吞错 | **PASS** — 探针失败仍落库；Redis 清理失败会抛错（生产有 Redis） |
| H3 | 无新密钥 | **PASS** |
| H4 | CUD 审计 | **PASS** — `watch_pin_pause` / `watch_pin_resume` / `watch_pin_ttl` / remove |
| H5 | 输入边界 | **PASS** — `Literal[8,24]`、`slot_index` 0–3 |
| H6 | 迁移 | **PASS** — ALTER + duplicate column 容错 |
| H7 | 产品风险 | **RISK 接受** — 4 槽均可永久暂停占用，需手动 X 释放（用户明确要求） |

## 逐模块走读

### 后端

| 模块 | 结论 | 备注 |
|------|------|------|
| `watch_repo._pin_still_occupied` | PASS | 暂停不占 TTL；开启监测才看过期 |
| `expire_due_pins` | PASS | 改为暂停 + end session，不 delete pin |
| `set_monitoring_enabled` + `_ensure_open_session` | PASS | TTL 到期后再开自动新 session |
| `_restore_idle_agent_watch` | PASS | 全局无监测才清 Redis |
| `replace_slot` | PASS | 保留槽位原 `ttl_hours`（审查项已修） |
| `probe_server_metrics` | PASS | Agent 基础指标优先；SSH 仅补 IO |
| `watch_probe_error_zh` | PASS | 与前端映射一致 |

### 前端

| 模块 | 结论 | 备注 |
|------|------|------|
| `WatchSlotCard` | PASS | 开关 + 8h/24h + 暂停态 UI |
| `tickCountdown` | PASS | 归零 `refreshPins`，不再误置 `empty` |
| `formatProbeError` | PASS | 状态/错误/来源中文化 |
| `WatchProbeRecordsTable` | PASS | 筛选器与列中文 |

## 审计中发现并修复

| 项 | 严重度 | 处理 |
|----|--------|------|
| `mock_watch_redis` 未 patch `watch_state.get_redis` | **P1** | 已修：`remove_slot` / `set_slot_monitoring` 单测 2 失败 → 19 passed |

## 遗留建议（非阻塞）

| 项 | 说明 |
|----|------|
| 设计文档 | 开关/暂停语义变更较大，建议补 `docs/design/specs/` 短文（当前仅 changelog） |
| HTTP 集成测试 | PATCH monitoring/ttl 的 404/422 路径未覆盖 |
| TTL 到期审计 | 后台 `expire_due_pins` 无 audit_log（可接受，session.end_reason=expired 可查） |
| PATCH 404 语义 | 「槽位无效」与「槽位为空」均 404，与 POST 422 不一致 |

## Closure

| H | 判定 | 依据 |
|---|------|------|
| H1–H6 | **PASS** | 鉴权、审计、迁移、探针落库均符合铁律 |
| H7 | **RISK 接受** | 用户要求暂停永久占槽 |

## DoD

- [x] `.venv/bin/pytest tests/test_watch_metrics.py tests/test_watch_pins.py tests/test_watch_probe_errors.py` — **19 passed**
- [x] `cd frontend && npm run type-check`
- [x] 审计修复：test fixture patch `watch_state.get_redis`
- [ ] `bash scripts/local_verify.sh` / 门控 7 道（部署前）
- [ ] 生产浏览器：开关暂停、24h 切换、到期自动暂停、中文错误展示

## 验证命令

```bash
.venv/bin/pytest tests/test_watch_metrics.py tests/test_watch_pins.py tests/test_watch_probe_errors.py -q
cd frontend && npm run type-check
bash deploy/pre_deploy_check.sh
```

## 合并建议

**Approve with notes** — 功能完整、测试绿、安全与审计达标；部署前跑门控 + 浏览器终验。遗留 HTTP 集成测试与设计文档可跟进 PR。
