# 2026-05-29 推送页面 Round 3 迭代

## 变更摘要
1. G1: 推送取消 — Redis 取消标记 + 前端"取消推送"按钮 + WS cancelled 状态
2. G2: 推送页面直接调度 — "⏰ 定时推送"按钮 + 时间选择模态框 + 调用调度 API
3. G3: 推送模板/收藏 — localStorage 模板存储 + 下拉选择 + 保存/删除
4. G4: 推送历史筛选分页 — 后端 page/per_page 参数 + 前端状态/服务器筛选 + 分页控件
5. G5: 浏览器推送完成通知 — Web Notification API + title 闪烁降级
6. G6: 拖拽上传 ZIP — 拖拽区 dragover/drop + 高亮 + click 回退

## 动机
推送页面 Round 1+2 已完成 10 项功能，但仍有 6 项体验/效率缺陷：推送无法取消导致只能等超时、定时推送需跳到调度页重新配置、常用推送组合每次手动选、历史无法筛选分页、离开页面不知推送结果、文件选择器不够直观。

## 涉及文件

### 后端
- `server/api/schemas.py` — 新增 `SyncCancel` 模型 (batch_id: str, max_length=32)
- `server/api/sync_v2.py` — 新增 `POST /api/sync/cancel` 端点
  - Redis SET `sync:cancel:{batch_id}` EX 3600
  - JWT 认证 + 审计日志
  - 返回 `{cancelled: true, batch_id: ...}`
- `server/api/websocket.py` — `broadcast_sync_progress()` 增加 `cancelled: int = 0` 参数
  - WS 消息新增 `cancelled` 字段，前端用于正确计算进度条
- `server/application/services/sync_engine_v2.py` — 推送取消检查
  - `_sync_one()`: 获取信号量后、rsync 前检查 `sync:cancel:{batch_id}`
  - 取消的服务器 status="cancelled"，创建 SyncLog 记录
  - 新增 `cancelled` 计数器，与 `completed`/`failed` 并列
  - 正常路径 WS 广播也传递 `cancelled` 计数
  - Redis 异常不再静默吞错，改用 `logger.warning()`
- `server/api/servers.py` — `get_all_sync_logs()` 参数改为 page/per_page + 返回 pages
  - 旧: `offset: int, limit: int` → 新: `page: int = 1, per_page: int = 20`
  - 返回增加: `page`, `per_page`, `pages` (ceil(total/per_page))

### 前端
- `web/app/push.html` — 6 项改动
  - G1: 推送进度区"取消推送"按钮 + `cancelPush()` + WS cancelled 状态渲染 ⊘
  - G2: "⏰ 定时推送"按钮 + 时间选择模态框 + `doSchedulePush()`
  - G3: 模板下拉框 + `saveTemplate()`/`applyTemplate()`/`deleteTemplate()` (localStorage)
  - G4: 历史区状态筛选下拉 + 服务器ID输入 + 分页控件
  - G5: 推送完成时 `new Notification()` + 授权拒绝时 title 闪烁降级
  - G6: 拖拽上传区 dragover/drop/dragleave + 高亮边框 + click 回退
  - 安全: `esc()` 函数增加单引号转义 `&#39;`，防止 onclick 属性 JS 注入

## 安全影响
- G1 (取消): Redis key 有 1h TTL 防泄漏；batch_id 为服务器生成的 UUID hex；取消端点有 JWT 认证 + 审计
- G2 (调度): 复用现有调度 API，已有 JWT 认证 + 审计
- G3 (模板): localStorage 仅存路径/服务器ID，不含密码/密钥
- G4 (分页): SQLAlchemy 参数化查询，无 SQL 注入风险
- G5 (通知): Notification API 需用户主动授权，非强制
- G6 (拖拽): 文件仍走 upload-zip 端点校验

## 审计修复
- F1: 取消服务器计数器 → 新增 `cancelled` 计数器，WS 广播含 `cancelled` 字段，前端进度条正确计算
- F2: 静默异常 → `except Exception: pass` 改为 `logger.warning()`
- F3: 异常链抑制 → 移除 `from None`
- F4: XSS 防御 → `esc()` 增加单引号转义

## 需要迁移/重启
- 是：后端 Python 需重启（supervisorctl restart nexus）
- 否：无数据库 schema 迁移

## 验证方式
1. G1: 推送 10 台 → 点击"取消推送" → 未启动的显示"已取消" ⊘ → 进度条正确
2. G2: 配置推送 → 点击"⏰ 定时推送" → 选择时间 → 调度页出现任务
3. G3: 保存模板 → 刷新 → 选择模板 → 表单自动填充
4. G4: 历史区筛选"失败" → 仅显示失败 → 分页翻页
5. G5: 推送完成 → 浏览器弹出通知
6. G6: 拖拽 ZIP 到上传区 → 高亮 → 释放上传成功

## 进度条
☑实现 ☑WSL验证 ☑审计8步 ☐部署 ☐健康检查 ☐浏览器验证 ☑changelog
