# 审计 — 调度表单批次 + Gate 7 交叉引用（2026-06-08）

**Changelog**（本批次）:

- `docs/changelog/2026-06-08-schedule-cycle-picker.md`
- `docs/changelog/2026-06-08-schedule-form-start-cycle-only.md`
- `docs/changelog/2026-06-08-schedule-once-cycle-aligned.md`
- `docs/changelog/2026-06-08-schedule-push-source-aligned.md`
- `docs/changelog/2026-06-08-schedule-server-category-picker.md`
- `docs/changelog/2026-06-08-schedule-cycle-time-merge.md`

**设计**: `docs/design/specs/2026-06-08-schedule-cycle-picker-design.md`

## 审计范围（本批次代码）

| 模块 | 文件 |
|------|------|
| 周期工具 | `scheduleCycle.ts` |
| 周期选择器 | `ScheduleCyclePicker.vue` |
| 调度页 | `SchedulesPage.vue` |
| 分组选机 | `ServerCategoryPicker.vue`（复用，无改） |
| 推送源 | `usePushForm.ts`（复用）、`PushZipUpload.vue` 等 |
| 错误提示 | `apiError.ts`、`useSnackbar.ts`、`index.ts` |
| 全局 UI | `App.vue` |
| 后端镜像 | `schedule_cycle.py` |
| 单测 | `test_schedule_cycle.py` |

## Gate 7 — HEAD 已提交（b8425cc 服务器排序）

下列文件已在 **上一提交** 变更，本审计显式列出 basename 以满足 `pre_deploy_check.sh` Gate 7：

`useServerPagination.ts` · `DashboardPage.vue` · `ServersPage.vue` · `servers.py` · `server_list_sort.py` · `server_repo.py` · `test_server_list_sort.py` · `web/app/index.html`

详审见 `docs/audit/2026-06-08-servers-live-sort-fix.md`。

## Step 3 规则扫描

| H | 规则 | 文件/点 | 结论 |
|---|------|---------|------|
| H1 | 调度 CRUD 鉴权 | `settings.py` 既有 JWT | PASS |
| H2 | Cron 注入 | `buildCronFromCycle` clamp + custom 5 字段 | PASS |
| H3 | 无秒级 roundtrip | 已移除 second-n；最短 1 分钟 | PASS |
| H4 | 单次/循环语义 | `run_mode` once→`fire_at`；cron→`cron_expr` | PASS |
| H5 | 推送源路径 | `coerce_nexus_host_abs_path` + 上传/验证与 Push 页一致 | PASS |
| H6 | 目标服务器 | `editorServerIds` + `JSON.stringify` 提交 | PASS |
| H7 | 错误可见 | `formatApiError` + 对话框 `saveError` + snackbar 非空 | PASS |
| H8 | 周期 UI 合并 | 单一「开始执行周期」区块（重复方式+时:分同行） | PASS |
| H9 | 静默吞错 | 保存/加载 catch 均 snackbar + 无空 `catch {}` | PASS |
| H10 | CUD 审计 | 调度 create/update/delete 仍写 audit_logs | PASS（后端未改） |

## Closure

| H | 判定 |
|---|------|
| H1–H10 | PASS |

| 门控 | 结果 |
|------|------|
| Gate 3 pytest schedule | PASS（`test_schedule_cycle.py` + `test_schedule_next_run.py`） |
| Gate 3 test_api schedules CRUD | PASS |
| frontend build | PASS |
| Gate 7 basename 交叉 | PASS（含 b8425cc 排序文件 + 本批次调度文件） |

## DoD

- [x] 设计文档 + 多份 changelog
- [x] `ScheduleCyclePicker` 宝塔式周期；单次默认；周期与时间合并
- [x] 推送源与 `#/push` 对齐（上传/ZIP/验证/暂存）
- [x] `ServerCategoryPicker` 按分类选目标机
- [x] 保存失败根因修复（`editorServerIds`、非空错误文案）
- [x] `cd frontend && npm run build`
- [x] 本审计覆盖 Gate 7 所需 basename
- [ ] 生产前端部署 + 用户浏览器终验

## 代码审读摘要

### SchedulesPage.vue

- `buildSchedulePayload`：校验名称/服务器/周期/源或脚本；once 模式 `computeNextCronRun`→`fire_at`
- `editorServerIds` 独立 ref，避免嵌套 `form.server_ids` v-model 不同步
- 对话框内 `saveError` v-alert，保存失败必可见

### ScheduleCyclePicker.vue

- 区块标题「开始执行周期」；`重复方式` + 时/分/第几分同一行
- 预览 `formatCycleLabel` + cron 字符串

### scheduleCycle.ts / schedule_cycle.py

- 与 aaPanel `GetCrondCycle` 对齐；无秒级；`validateCycleConfig` 前端校验

### apiError.ts / useSnackbar.ts / App.vue

- `normalizeDetail` / `formatApiError` 禁止空字符串；snackbar `z-index: 10000`

## 风险与回滚

| 风险 | 缓解 |
|------|------|
| 旧调度 custom Cron | custom 模式保留原表达式 |
| 单次 fire_at 时区 | ISO UTC 与 runner 60s 轮询一致 |
| 前端-only | 回滚 `web/app/` 静态资源即可 |
