# H3 Console 给 `minimax-h3/` 的四后端 API(`gateway.py` + 4× `api.py`)加一层 Web 管理台: 左侧任务记录列表 + 右侧新建/详情,上传首尾帧/参考图 → 提交生成 → 点开任务看状态和结果。 不重做调度,只加 gateway.py 没有的东西(Web UI、文件上传、任务持久化、按需 fl2va/ref2va 弹性分卡)。 需求/架构决策见 [`DESIGN.md`](DESIGN.md)——**先读那份**,尤其「四点五、弹性 partition 调度器」。 ## 页面结构 左侧栏固定:顶部一条**新建任务**(选中态=当前在填新表单),下面是任务记录列表,按提交时间倒序, 点哪条就直接跳到那条的详情。右侧是新建表单(三个 tab:文生/首尾帧/参考图)或任务详情二选一。 URL 是真实可分享的路径,不是纯前端状态: - `/newtask.html`(或裸 `/`)— 新建任务页 - `/task.html/<任务id>` — 某个任务的详情页,刷新、直接打开、发链接给别人都能定位到同一条 这三条路径背后是**同一个** `static/index.html`,`server.py` 里 `/task.html/{tid}` 和 `/newtask.html` 两条路由原样返回这份文件,靠前端 JS 读 `location.pathname` 决定渲染哪个视图——不是三个页面文件, 省得同一份侧边栏/样式在多个 html 里重复维护。提交完新任务会自动跳到 `/task.html/<新id>`。 ## 状态(2026-08-20) **已部署到服务器,端口 8080,跑在 `/data/models/minimax-h3/console/`,弹性调度器全链路验证通过。** 本地冒烟测试:上传图片 → 拒绝非图片文件 → 提交生成任务 → 轮询到 `done` → 下载代理拿到合法 mp4 → sqlite 影子记录核对无误 → 浏览器打开首页、三个 tab 切换、任务列表内联播放,均正常。 **服务器上用真实 GPU 做的弹性调度全链路验证**(这部分本地测不了,见 DESIGN.md「四点五」): 上传首帧图 → 提交 fl2va 任务(当时四卡全 ref2va)→ console 正确返回 `queued`(不是 503)→ supervisor 30 秒后选中一个空闲 ref2va 后端 flip 成 fl2va → reconciler 在新后端 ready 后自动把 排队的任务提交上去 → 任务 `running` → `done` → 通过 console 下载代理拿到合法 mp4。全程无人工干预。 ### 部署过程中揪出的两个真实 bug(修完才让上面这条链路跑通) 1. **`_flip_backend` 里的 PID 提取从来没匹配上过**:`ss -lptn` 的输出里 pid 不是独立的空格分隔 token,是嵌在 `users:(("python",pid=NNNN,fd=14))` 里面的——`tok.startswith("pid=")` 永远不命中, `kill` 从来没真的执行。后果是"flip"只是在已被占用的端口上起了一个新 uvicorn 进程,因端口冲突 立刻退出,旧进程全程没受影响——不是数据损坏,但功能是假的。改用 `re.findall(r"pid=(\d+)", ...)`。 2. **弹性调度器不知道"已经在切换中",会对同一个需求连续 flip 好几张卡**:api.py 冷启动要 13-20 秒,跨了不止一个 10 秒的 tick。中间几轮 tick 看到目标分区的 ready 数还是 0, 误判成需求没满足,接着把第二张、第三张卡也切了过去——一个 `pending=1` 的请求切了三张卡。 加了 `flipping: dict[port, (目标分区, 发起时间)]` 记账,效果数(ready + 正在切换中的) 满足需求就不再触发新的 flip,60 秒还没起来算超时放弃。 3. **`pkill -f ""` 杀掉了自己所在的 SSH 会话**:这条不是 console 代码的 bug,是我操作 服务器时踩的坑——`start-all.sh` 注释里其实早就警告过这个("pgrep -f 会匹配到自己那条命令行"), 这次是真的踩了一遍:`pkill -f "uvicorn server:app.*8080"` 这条命令的完整命令行本身就包含 被搜索的那串文本,`pkill -f` 把自己所在的 shell 一起杀了,SSH 连接直接断,命令链后半截全没执行。 之后全程改用 `ss -lptn "sport = :PORT" | grep -o "pid=..."` 按端口精确定位再 kill,不再用 `pkill -f`。 ### 播放/下载查出来的三个坑(用户反馈"播放和下载好像有问题"后排查,均已修) 1. **`download`/`poll` 字段抄了 gateway 内部 id**:`GET /v1/videos/{id}` 原样带回 `api.py`/`gateway.py` 响应里的这两个字段,值是 gateway 内部那个 id,不是 console 自己对外用的 id——直接用这两个字段 拼链接会 404。页面本身没受影响(前端一直是自己用 `j.id` 拼的 URL,没读这两个字段),但接口本身 是错的。`_row_view()` 里统一改成用 `row['id']`(console 自己的 id)重新拼。 2. **详情页每 5 秒轮询会把播放到一半的 `