Files
Nexus/docs/design/plans/2026-05-29-push-round4.md
2026-07-08 22:31:31 +08:00

5.5 KiB

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):

    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. 上传区域下方加:
    <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 旁加:
    <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):

    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):

    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