# 2026-05-29 推送页面迭代 Round 4 — 技术文档

## 涉及文件清单

| 文件 | 改动类型 | 涉及功能 |
|------|---------|---------|
| `server/api/schemas.py` | 新增模型 | H3 diff + H4 diagnose + H5 validate |
| `server/api/sync_v2.py` | 新增端点 | H3 file-diff + H4 diagnose + H5 validate |
| `web/app/push.html` | 修改 | H2 搜索 + H3 diff UI + H4 诊断 UI + H5 源路径 + H6 批量重试 |
| `docs/changelog/2026-05-29-push-round4.md` | 新增 | changelog |

## 实现步骤

### Step 1: H2 服务器搜索过滤（纯前端）

**文件**: `web/app/push.html`

1. `<select id="targetServers">` 上方加 `<input id="serverSearch" placeholder="搜索服务器...">`
2. 新增 `filterServerOptions()` 函数：读取搜索词，遍历 `<select>` 的 `<option>`，按名称/域名匹配隐藏/显示
3. `filterServers()` 加载完成后调用 `filterServerOptions()`
4. 搜索框 `oninput` 事件绑定 `filterServerOptions()`

### Step 2: H5 源路径直接输入（后端+前端）

**后端**:

1. `server/api/schemas.py` — 新增 `ValidateSourcePath(BaseModel)`:
   ```python
   class ValidateSourcePath(BaseModel):
       path: str = Field(..., min_length=1)
   ```

2. `server/api/sync_v2.py` — 新增 `POST /api/sync/validate-source-path`:
   - 安全: `os.path.realpath()` 校验，禁止 `/etc`, `/root`, `/home`, `/var` 等敏感前缀
   - 验证: 存在 + 是目录 + 统计文件数和大小
   - 返回: `{valid, is_dir, file_count, size_bytes}`
   - 审计日志

**前端**:

1. 上传区域下方加:
   ```html
   <div class="flex items-center gap-2 mt-2">
     <span class="text-xs text-slate-500">或输入源路径:</span>
     <input id="sourcePathInput" placeholder="/path/to/files" class="flex-1 ...">
     <button onclick="validateSourcePath()">验证</button>
   </div>
   ```
2. `validateSourcePath()`: 调用 API → 成功后设置 `_uploadSourcePath` + 显示验证结果
3. `clearUpload()` 同时清空源路径输入框

### Step 3: H6 批量重试（纯前端）

**文件**: `web/app/push.html`

1. 进度条区域，`countFailed` 旁加:
   ```html
   <button id="retryAllBtn" class="hidden text-xs text-brand-light hover:underline" onclick="retryAllFailed()">🔄 重试全部失败</button>
   ```
2. `updateProgressFromWS/Result()` — 有失败时显示 `retryAllBtn`，文本显示失败数
3. 新增 `retryAllFailed()`:
   - 收集所有可见的 `srv_retry_{id}` 按钮的 `retryJobId`
   - 逐个调用 `POST /api/retries/{id}/retry`
   - 显示进度 toast

### Step 4: H4 推送排错面板（后端+前端）

**后端**:

1. `server/api/schemas.py` — 新增 `SyncDiagnose(BaseModel)`:
   ```python
   class SyncDiagnose(BaseModel):
       server_id: int = Field(..., ge=1)
       target_path: Optional[str] = None
   ```

2. `server/api/sync_v2.py` — 新增 `POST /api/sync/diagnose`:
   - 检查项（依次执行，遇短路可提前返回）:
     a. SSH 连通性: 尝试 `exec_ssh_command(server, "echo ok", timeout=10)`
     b. 磁盘空间: `df -h {target_path}` → 解析 avail/usage%
     c. 目标路径: `ls -ld {target_path}` → 解析权限 + owner
     d. 写入测试: `touch {target_path}/.nexus_test_$$ && rm {target_path}/.nexus_test_$$`
   - 返回: `{ssh_ok, ssh_error?, disk_avail, disk_used_pct, path_exists, path_perms, path_writable, errors[]}`
   - 审计日志

**前端**:

1. `showProgress()` 每行加 `srv_diag_{id}` 诊断按钮（仅失败时显示）
2. 新增 `diagnoseServer(serverId)` — 调用 API → 弹出模态框显示检查结果
3. 模态框 `#diagModal`: 4 项检查，每项 ✓/✗ 图标 + 详情

### Step 5: H3 推送对比 Diff 视图（后端+前端）

**后端**:

1. `server/api/schemas.py` — 新增 `FileSyncDiff(BaseModel)`:
   ```python
   class FileSyncDiff(BaseModel):
       server_id: int = Field(..., ge=1)
       source_path: str = Field(..., min_length=1)
       relative_path: str = Field(..., min_length=1)
       target_path: Optional[str] = None
   ```

2. `server/api/sync_v2.py` — 新增 `POST /api/sync/file-diff`:
   - 安全: `os.path.realpath(local_file)` 必须 `startswith(source_path)` 且 source_path 必须 `startswith("/tmp/nexus_upload_")`
   - 读本地文件: 最多 100KB
   - 读远程文件: `cat {shlex.quote(remote_file)}` via SSH，最多 100KB
   - diff 计算: Python `difflib.unified_diff()` → 返回 `diff_lines: [{type: "add"|"del"|"ctx", content}]`
   - 返回: `{file_name, local_exists, remote_exists, local_size, remote_size, diff_lines, truncated}`
   - 审计日志

**前端**:

1. `renderPreviewResult()` verbose 文件列表中每行加 "📄 diff" 按钮
2. 新增 `showFileDiff(serverId, relativePath)` — 调用 API → 弹出 diff 模态框
3. 模态框 `#diffModal`: 文件名 + 左右对比或逐行显示（新增绿底、删除红底、上下文灰底）

## 依赖与配置变更

无新依赖。无数据库 schema 迁移。

## 测试要点

1. H2: 输入 "web" → 下拉只显示名称/域名含 "web" 的服务器
2. H5: 输入 `/tmp/nexus_upload_xxx` → 验证成功 → 可推送
3. H5: 输入 `/etc` → 验证拒绝（敏感路径）
4. H6: 推送 3 台失败 2 台 → 点"重试全部失败" → 2 台重试
5. H4: 推送失败 → 点"🔍 诊断" → SSH/磁盘/权限检查结果
6. H3: 预览 → 文件列表 → 点 "diff" → 显示逐行对比
7. H3: 本地有文件远程无 → 显示为纯新增（全绿）
8. H3: 本地无远程有 → 显示为纯删除（全红）

## 回滚方式

所有改动均为增量（新增函数/端点/前端元素），回滚只需：
1. `git revert` 提交
2. `supervisorctl restart nexus`
