Files
DCTS/docs/workflow_detail_design.md
T
fmq cd370d88e7 feat(all): 任务引擎双阶段解耦、僵尸涡旋修复、动态 CPU 配额与前端详情页重构
将 TLUSTY/SYNSPEC 拆为各自独立的 enabled/policy/strategies 阶段,
以策略链自动弹栈取代单级 seed_step 布尔回退;定向修复 2026-08-02
僵尸任务涡旋事故;新增节点并发配额热调;前端详情页从 1412 行巨型
视图拆为薄控制器 + detail 子模块,并补齐工具层与单测。

引擎与调度(task_engine_decoupling_design.md)
- models.rs: 新增 StagePolicy / EngineStageConfig / TaskSpec 阶段字段、
  normalize_compat() 校正旧版在途消息策略链、failed_stage 归因
- scheduler.rs: resolve_dispatchable_chain 派发门控、
  trigger_strategy_fallback 按 failed_stage 精确弹栈;启动期
  force_recompute/skip_converged(默认)/skip_failed 三策略
- db.rs: tasks 表 +7 列持久化阶段配置;终态守卫
  (mark_grid_point_running 仅 pending/queued→running;
  record_task_report 拒绝迟到失败翻黑 converged);策略弹栈快照

僵尸涡旋修复(runbook-20260802-zombie-vortex-fix.md)
- 全链路跨库活性交叉校验:派发/claim/孤儿回收/回退统一查 MQ 队列活性,
  活则放行、死则清僵尸,结构性消除"每点重复派发"
- stop/重启卫生:清队列同步 delete_tasks_by_ids,杜绝遗留 pending 行
- report_task: 幂等吸收 + 409 区分迟到冗余结果,仅 state_changed 时回退
- MQ: NULL workflow_name 回填 __legacy__、requeue 后迟到上报被 403 竞态修复

动态 CPU 配额(dynamic_cpu_slots_design.md)
- admin.rs: POST /admin/nodes/:id/quota(Option<Option<i32>> 区分
  缺字段/显式 null);nodes 表 +admin_max_slots
- worker.rs: effective_max_slots = min(admin, physical),心跳下发原子生效

科学产物保全(tlusty_result_artifacts.md)
- runner.rs: SYNSPEC 启动前快照 fort.12/fort.14 → .bfac/.emflux 防覆盖
- 半失败点(大气收敛+光谱失败)改判 Failed 并写入 note;仅 SYNSPEC
  场景不再恒判失败;撤销归档 LRU 200 上限改为永久保留
- executor.rs: 透传 synspec_params 数值参数(此前固定 None)

前端(dashboard/)
- workflowDetail.js 1412→328 行,拆出 views/detail/{ctx,overview,
  pointsTable,parSets,pointPanel}.js,AbortController 治理监听/请求生命周期
- 删除 wfActions.js,新增 wfEnginePanel.js(双阶段三维配置编辑面板)
- 新增 utils/{errors,format,icons,polling,yamlStage}.js 纯函数模块
- 路由级动态 import 代码分割;节点配额三点菜单 + Modal 管理
- 首次引入 node:test 单测(format/polling/yamlStage/psCache,644 行)
- 系统性补齐 a11y:skip-link、ARIA、Tab 键盘漫游、toast 关闭、退出动画

文档与工具
- 新增 6 篇设计/调研:引擎解耦、动态配额、涡旋 runbook、
  光谱正确性分析、收敛判断、产物归档
- PIPELINE/design/api/database 等协同重写为分布式 C/S 架构口径
- scripts/fetch_results.sh 跨节点产物备份;import_results 按 cno 升序导入
- workflows/sdB_cno.yaml: 新增 tlusty/synspec_stage 配置块,修正 wstart 笔误
2026-08-04 23:40:52 +08:00

407 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 工作流执行可视化 —— 功能重设计文档
> 目标:把"恒星大气网格工作流"面板从当前的 **3 个动作(查看 YAML / 启动 / 删除)**
> 升级为完整的 **执行可观测台**:执行进度、逐网格点执行详情、收敛判定、
> 冷启动 vs 种子步进归因、参数空间冷启动成功图谱,以及失败点重试。
>
> 原则:**数据已经在库里大半,缺口主要是 HTTP 暴露 + 少量持久化 + 全新前端 UI。**
> 零新增前端依赖(不引入图表库),沿用现有 ESM + 原生 DOM + 设计令牌风格。
> ## ⚠️ 实现状态(2026-08 更新,与当前 dashboard 实现的差异)
>
> 本文为**设计文档**,部分设计已被后续实现与 `task_engine_decoupling_design.md` 的
> 引擎面板设计覆盖,按现状对齐如下:
>
> - **已实现**:详情页 4 个 Tab(概览 overview / 逐点表 pointsTable / 平行集合分析
> parSets / 点详情面板 pointPanel)、进度曲线与 ETA、停滞检测横幅、CSV 导出、
> `workflow_progress_snapshots` 表、YAML 编辑器(yamlEditor,双模式/语法高亮/运行中
> 锁定/脏状态守卫)、引擎配置面板(wfEnginePanelTLUSTY/SYNSPEC 双阶段卡、enabled/
> policy/strategies 编辑、折叠跨轮询保持)。
> - **已变更(设计被覆盖)**:首页卡片操作按钮与详情页顶部控制条按钮**已全部移除**,
> 操作收敛至详情页内嵌引擎面板(见 `task_engine_decoupling_design.md §6`);Tab 3 由
> **2D 热力矩阵改为 Parallel Sets 平行集合图**(§2.2 Tab 3 描述过时);Tab 1 方法归因
> 行已移除(cold/seed 计数在点表与 parSets 中可用)。
> - **未实现**:§2.2/§2.4 的「重试失败点」按钮与 `POST .../points/retry` 端点
> (前后端均未实现)。
> - **方法归因**`imported` 不再作为独立收敛途径(导入点按 seed_step 统计),
> `success_method` 过滤白名单仅 `cold_run` / `seed_step`。
---
## 1. 现状盘点(带代码证据)
### 1.1 已经持久化、可直接用的数据
| 数据 | 位置 | 说明 |
|---|---|---|
| 网格点 6 维参数、`cno_sum``wave` | `grid_points` 表(`db.rs:246-263` | 按 `workflow_name` 分区,复合唯一 `(workflow_name, name)` |
| 点状态 `pending/queued/running/converged/failed` | `grid_points.status` | `GridPointStatus``models.rs:235-243` |
| **成功手段** `cold_run` / `seed_step` / `imported` | `grid_points.success_method` | 收敛时由成功任务的 `task_type` 回填(`db.rs:1097-1101`);失败点为 NULL |
| 重试次数 | `grid_points.attempt_count` | 每次回报 +1`db.rs:1089-1095` |
| 每次尝试的收敛指标 | `tasks.max_relc` | 收敛判据 `max_relc < chmax`(默认 0.001`conv_check.rs:105-109` |
| **每次尝试的种子来源** | `tasks.seed_point_name` | 派发时写入(`scheduler.rs:194-205` |
| 每次尝试的方法 / 错误 / 节点 / 完成时间 | `tasks.task_type / error_message / node_id / completed_at` | 一个点多行(每次尝试一行),工作流删除前长期保留 |
| 全局种子库 | `seeds` 表 + `data/seeds/<name>/<name>.7` | 仅收敛且无 NaN 的点入库(`task.rs:187-207` |
| **逐阶段完整诊断**lte→nc→nl / seed_nc→nl | `data/seeds/<name>/conv.json` | `ModelSummary``models.rs:376-391`):每阶段 `converged / best_max_relc / chmax / elapsed_sec`、总耗时、`synspec_sec`、所用种子。**成功与失败的点都会写**(`task.rs:178-184` |
| 按工作流的统计聚合 | `db.get_grid_summary_stats(Some(wf))` | **已实现且有单测**`db.rs:1582-1639`),但没有任何 HTTP 端点调用它 |
### 1.2 缺口(需要补的)
| # | 缺口 | 影响 | 补法 |
|---|---|---|---|
| G1 | 无逐点 HTTP 端点 | 前端拿不到任何点级数据 | 新增 `/workflows/:name/points` |
| G2 | 无按工作流的统计端点(`get_grid_summary_stats` 未暴露;`/api/status` 只有全局) | 卡片/详情无法显示每工作流进度 | 新增 `/workflows/:name/stats`;列表接口内联计数 |
| G3 | `queued` 被并进 `pending``db.rs:1596,1612` | 无法区分"排队中"与"未入队" | 新端点拆开统计 |
| G4 | 单点耗时/迭代数未进库(`TaskReport.elapsed_sec` 收到即丢;`last_iter``StageSummary` 前被丢弃) | 列表无耗时列、无 ETA | 迁移补列 + runner 携带 `last_iter`Phase 3 |
| G5 | 无进度时间序列 | 画不出"进度-时间"曲线 | 新表 `workflow_progress_snapshots`Phase 3 |
| G6 | 失败点无法重试(start 只重置 `queued``db.rs:886-899`) | 失败点永久卡死 | 新增 retry 端点(复用 `reset_specific_grid_points_to_pending` 思路) |
| G7 | conv.json 不提供下载/查看 | 逐阶段诊断无法展示 | 点详情端点读盘解析返回 |
| G8 | 前端无任何进度/详情 UI;无 tab 组件 | —— | 全新 UI |
### 1.3 领域语义(设计依据)
- **冷启动 vs 种子步进不是用户选的,是调度器按"种子可用性"自动决定的**:
派发时 `find_best_seed_from_db` 命中(同族精确匹配优先,否则全局加权距离 ≤ 3.0,
`seed_finder.rs:13-38`)→ `SeedStep`(热启动链 `seed_nc→nl`),否则 `ColdRun``lte→nc→nl`)。
- **wave = 难度波次**:按 `cno_sum` 升序分组,"先易后难",让早期收敛点充当种子。
所以"哪些参数冷启动能成功" ≈ 低 cno_sum / 中低温区的点;高温 He-poor 区几乎全靠种子步进。
- **还有一次性失败回退**:冷启动发散且存在邻居种子时,自动以 `seed_step` 重投一次
`scheduler.rs:270-350`,受 `seed_step_fallback` 配置与 `has_seed_step_attempt` 一次性闸门约束)。
因此 `success_method='seed_step'` 的点可能是"冷启动失败后被救回"的——该归因需从 `tasks` 多行历史还原。
- **第三类 `imported`**:历史 `run_grid.py` 结果导入(`db.rs:966-981`),UI 需单独呈现为"历史导入"。
- **收敛判据**:末次迭代最差深度点的最大相对修正 `max_relc < chmax`(默认 1e-3),
且大气 NaN 占比 ≤ 10%`conv_check.rs:127-149`)。
---
## 2. 功能设计(信息架构)
### 2.1 工作流卡片(改造现有 `workflows.js` 卡片)
```
┌─────────────────────────────────────────────┐
│ sdB_cno [运行中] │ ← 现有:名称/描述/状态徽章
│ sdB 6维恒星大气模型计算网格 │
│ ▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░ 42% │ ← 新增:收敛进度条 + 百分比
│ 181/432 收敛 · 12 种子步进 · 3 失败 · 8 运行 │ ← 新增:一行计数摘要
│ [进入详情 →] [查看YAML] [暂停] [删除] │ ← 整卡头部可点进详情页
└─────────────────────────────────────────────┘
```
- 卡片名/头部区域是 `<a href="#/workflows/sdB_cno">`;操作按钮 `stopPropagation` 保持独立语义。
- 进度数据来自 **列表接口内联计数**(见 §3.2),不额外轮询。
- 状态徽章扩充:`initializing` 显示"初始化中"warning 琥珀),`paused` 显示"已暂停"secondary),
不再把三者都归为"闲置"。
### 2.2 工作流详情页(独立页面,hash 路由 `#/workflows/:name`
> **信息架构决策**:详情内容量(三 Tab + 可过滤点表 + 热力矩阵 + 逐点诊断)远超模态框承载,
> 且需要深链/返回键/长时间监控驻留。因此采用**独立视图页面** + 零依赖 hash 路由,
> 而非宽模态。入口:卡片整卡或"详情"按钮 → `location.hash = '#/workflows/<name>'`。
**页面骨架**(顶部常驻"任务控制条",是全页最先映入眼帘的部分——直接呈现这个网格的实时生命体征):
```
← 返回控制台 sdB_cno [运行中] [启动/暂停] [重试失败] [查看YAML] [删除]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━▓▓▓▓▓▓░░░░░░░░░░░░░░░░░░░ ← 分段进度条:绿=收敛 红=失败 琥珀=运行 蓝=排队 灰=待定
432 网格点 · 181 收敛(42%) · 8 运行 · 40 排队 · 3 失败 · 冷启动 169 / 种子步进 12 · ETA ≈ 2h40m
```
- 分段进度条按状态占比着色拼接,5s 轮询下各段宽度此消彼长——进度是"看得见在动"的。
- 计数用 `tabular-num` 防跳动;数字变化可加轻量过渡。
- 工作流不存在(hash 指向已删除的名字)→ 空态卡 + 返回按钮。
**Tab 1 — 执行概览**
- 4 个 metric tile(复用 `.metric-card`):网格总数 / 已收敛 / 失败 / 运行中+排队。
- **方法归因**:冷启动收敛 X · 种子步进收敛 Y · 历史导入 Z · 失败 N(彩色计数行)。
- wave(难度波次)分布:每波次 `converged/total` 的小条形列表——直观看到"难度推进到哪了"。
- **最近动态流**:最近 12 条尝试回报(点名 / 方法徽章 / 结果 / max_relc / 节点 / 完成时间),
轮询下新条目从顶部淡入——数据就是 `points` 端点 `sort=last_completed_at&order=desc&limit=12`,零新增端点。
- ETA(Phase 3):按平均单点耗时 × 剩余点数 ÷ 集群空闲槽位估算。
**Tab 2 — 网格点明细**
- 过滤器:状态(全部/排队/运行/收敛/失败)、方法(全部/冷启动/种子步进/导入)、wave 下拉、点名搜索框。
- 表格(复用 `.data-table` + 关键行增量刷新,仿 `nodesTable.js`):
| 列 | 来源 |
|---|---|
| 点名(mono 省略,title 全文) | `name` |
| Teff / logg / logHe / CNO和 | 6 维参数 |
| wave | `wave` |
| 状态 | badge:收敛=online 绿 / 失败=danger 红 / 运行=warning 琥珀 / 排队=info 蓝 / 待定=secondary |
| 方法 | badge:冷启动=蓝(blueprint/ 种子步进=紫(purple/ 导入=secondary——**刻意区别于状态色** |
| max_relc | `tabular-num`,科学计数;失败点显示最后一次尝试值 |
| 尝试数 | `attempt_count`>1 且收敛 = "被救回",加 ↻ 角标) |
| 耗时 | Phase 3 起有值;之前用 `completed_at-created_at` 粗估并标 ~ |
| 操作 | [查看] → 点详情 |
- 分页:`limit=100 + offset`,服务端排序(默认 wave→cno_sum→teff)。
- **刷新策略**:打开弹窗时拉一次;Tab 激活期间每 5s 自动刷新(可手动刷新按钮强制);
弹窗关闭即停。不污染全局轮询。
**Tab 3 — 收敛性分析(冷启动图谱)**
- 核心问题:"哪些参数范围内冷启动能成功"。
- 6 维空间降到 2D 热力矩阵:用户从下拉选 X 轴 / Y 轴(默认 X=Teff、Y=cno_sum);
其余维度若为多值则提供切片选择器(固定某维取值)。
- 单元格着色(CSS grid,无图表库):
- 🟩 冷启动收敛(`converged && method=cold_run`
- 🟪 种子步进收敛(`seed_step`
- ⬜ 历史导入(`imported`
- 🟥 失败(`failed`
- 🟨 运行/排队中
- ░ 未开始(`pending` 且从未尝试)
- 矩阵下方:图例 + 聚合结论条("冷启动成功率 62%268/432);Teff≥60000 区间冷启动成功率 0%,全部由种子步进救回 41 个、失败 3 个")。
- 单元格 hover 显示点名与 max_relc;点击 → 同 Tab 2 的点详情。
### 2.3 网格点详情(右侧滑入面板 slide-over,不再模态套模态)
点表格/热力图/动态流中任一点名 → 右侧滑入 480px 面板(`.point-panel`,背景遮罩 + Esc/点击遮罩关闭,
复用焦点圈管理;独立页面上滑入面板比二级模态更轻、视线不跳转)。
- 头部:点名(mono)+ 状态/方法徽章 + [复制点名]。
- **尝试历史表**`tasks` 多行):# / 方法 / 种子来源(点名,可点击跳查该点)/ max_relc / 结果 / 节点 / 完成时间 / 错误摘要。
这一张表直接还原"冷启动失败 → 种子步进救回"的完整剧情。
- **逐阶段链诊断**(读 `conv.json`):每阶段一张小卡:
`lte` ✅ grey start · `nc` ⚠️ max_relc=0.957(不要求收敛)· `nl` ✅ max_relc=6.9e-3 / 17 iter / 612s
种子步进链则显示 `seed_nc → nl`,并标注种子点名与总耗时、synspec 耗时、NaN 标志。
- 失败点:突出显示最后一次 `error_message``code-pre-wrap`textContent 渲染防 XSS)。
### 2.4 视图与操作逻辑
**路由**(新增 `src/router.js`,约 70 行,零依赖):
| hash | 视图 | 挂载动作 | 卸载动作 |
|---|---|---|---|
| `#/`(或空/未知) | 控制台首页(现有布局) | 渲染骨架 + `startPolling()` | 停全局轮询 |
| `#/workflows/:name` | 工作流详情页 | 渲染页面骨架 + 拉 stats/points + 启作用域轮询 | 停作用域轮询 |
- `window.addEventListener('hashchange', render)`;首次加载按当前 hash 渲染。
- **登录门禁不变**:无 token 时任何 hash 都先展示登录遮罩,登录成功后按 hash 进入对应视图。
- 全局 header/footer 保持常驻;视图内容渲染进 `#view-root` 容器(`index.html` 结构调整:
现有 `main.main-container` 内容收敛为首页视图模板)。
- 返回键/深链/刷新天然可用(hash 不触发整页刷新)。
**交互一览**
| 操作 | 前端 | 后端 |
|---|---|---|
| 进详情页 | hash 切换 → 按需 fetch stats + points(第1页) | —— |
| 切 Tab | 委托 `data-wf-tab`;切到"明细/分析"时惰性加载 | —— |
| 过滤/翻页/排序 | change/click → 重新 fetch(带 query | 端点支持参数 |
| 自动刷新 | 详情页挂载期间 5s 拉 stats(控制条+概览常驻刷新);明细 Tab 激活时同步刷新表格 | —— |
| 点详情面板 | 行内 [查看]/点名链接 → 拉 `points/:point` → 滑入面板;种子来源点名可跳查(换载面板内容) | —— |
| 重试失败点 | `showConfirm` → POST → toast → 刷新 stats/points | 新端点,仅 `running` 状态允许 |
| 返回首页 | "← 返回控制台"链接 / 浏览器返回 | —— |
| 首页卡片 | 整卡点击进详情;卡片上保留 启动/暂停/删除 快捷操作 + 查看YAML(仍为模态) | —— |
---
## 3. 后端设计
### 3.1 新增端点(全部挂在 `/api/workflows/:name/...` 下)
> 鉴权:`required_role` 里 `path.starts_with("/workflows/")` 已映射 Admin`mod.rs:92-95`),
> 子路由**无需改鉴权矩阵**。统一返回 `{success, message, data}` 信封(与现有 workflow API 一致)。
> 点名参数复用现有字符集白名单校验(`task.rs:133-146` 同款),防路径穿越。
#### ① `GET /api/workflows/:name/stats` —— 每工作流进度(G2/G3)
复用并增强 `get_grid_summary_stats(Some(wf))`**拆开 pending/queued**,补方法与波次维度:
```json
{
"success": true, "message": "ok",
"data": {
"name": "sdB_cno", "status": "running",
"total": 432,
"pending": 120, "queued": 40, "running": 8, "converged": 261, "failed": 3,
"cold_run_converged": 220, "seed_step_converged": 41, "imported_converged": 0,
"waves": [ {"wave": 0, "total": 108, "converged": 108, "failed": 0}, ... ],
"avg_point_sec": 740.0, "eta_sec": 9600
}
}
```
- `avg_point_sec` / `eta_sec`Phase 3 有 `elapsed_sec` 列后精确计算;
之前用 `tasks.completed_at - tasks.created_at` 均值近似(含排队等待,偏大,标注近似)。
- SQL 走现有覆盖索引 `idx_grid_points_wf_status`,单次聚合扫描,5s 轮询无压力。
#### ② `GET /api/workflows/:name/points` —— 逐点列表(G1
参数:`status``method``wave``q`(点名子串)、`sort``wave|teff|max_relc|attempts`,默认 wave)、
`order``limit`(≤500,默认 100)、`offset`
```json
{
"success": true, "message": "ok",
"data": {
"total": 432,
"points": [
{
"name": "t60000_g5.0_he-2_c-4_n-4_o-4",
"teff": 60000, "logg": 5.0, "loghe": -2, "logc": -4, "logn": -4, "logo": -4,
"cno_sum": -12, "wave": 0,
"status": "converged", "success_method": "seed_step", "attempt_count": 2,
"last_max_relc": 0.00043, "last_task_type": "seed_step",
"seed_point_name": "t60000_g5.0_he2_c-4_n-4_o-4",
"node_id": "node-a1b2", "last_completed_at": "2026-07-30T11:12:00Z",
"elapsed_sec": 126.4
}
]
}
}
```
- "最近一次尝试"用关联子查询取 `tasks` 最新行:
```sql
SELECT gp.*, t.max_relc AS last_max_relc, t.task_type AS last_task_type,
t.seed_point_name, t.node_id, t.completed_at AS last_completed_at,
t.error_message AS last_error
FROM grid_points gp
LEFT JOIN tasks t ON t.task_id = (
SELECT t2.task_id FROM tasks t2
WHERE t2.point_name = gp.name AND t2.workflow_name = gp.workflow_name
ORDER BY t2.completed_at IS NULL, t2.completed_at DESC, t2.created_at DESC
LIMIT 1)
WHERE gp.workflow_name = ?1 [AND ...filters]
ORDER BY gp.wave, gp.cno_sum, gp.teff
LIMIT ? LIMIT OFFSET ?;
```
- 配套新索引:`CREATE INDEX idx_tasks_point_wf_time ON tasks(point_name, workflow_name, completed_at);`
(迁移中补;432 点规模下无索引也能跑,加索引是为大网格兜底。)
- 备选(若大网格性能不够):把 `last_*` 字段反规范化进 `grid_points`,在 `record_task_report` 里顺手写——
但**默认不做**,避免迁移面扩大;先用 JOIN,压测后再议。
#### ③ `GET /api/workflows/:name/points/:point` —— 单点详情 + 阶段诊断(G7)
```json
{
"success": true, "message": "ok",
"data": {
"point": { /* 的单点字段 */ },
"attempts": [
{"task_id": "...", "task_type": "cold_run", "seed_point_name": null,
"status": "failed", "max_relc": 954000.0, "atmosphere_has_nan": true,
"node_id": "node-a1b2", "error_message": "nl stage diverged...",
"created_at": "...", "completed_at": "..."},
{"task_id": "...", "task_type": "seed_step", "seed_point_name": "t60000_...",
"status": "completed", "max_relc": 0.00043, ...}
],
"conv": { /* data/seeds/<name>/conv.json 解析后的 ModelSummary;文件不在则 null */ }
}
}
```
- `conv` 读盘路径严格拼为 `seeds_dir/<point>/conv.json`(单层目录,point 先过白名单 +
canonicalize 归属兜底),解析失败/不存在 → `null`(前端降级为"诊断文件不可用")。
**不**把整文件塞进列表接口(体积控制)。
#### ④ `POST /api/workflows/:name/points/retry` —— 失败点重试(G6
请求体:`{"names": ["t80000_..."], "all_failed": true}`(二选一)。
- 前置:工作流必须 `running`(否则 400,提示"请先启动工作流"——避免重置后无调度器消费的僵尸态)。
- 逻辑:新 db 函数 `reset_failed_points(wf, names|all)`
`UPDATE grid_points SET status='pending', success_method=NULL WHERE workflow_name=? AND status='failed' [AND name IN (...)]`
随后立即触发一次 `schedule_pending_tasks()`(不必等 30s tick)。
- 重试后的方法仍由调度器按种子可用性自动决定(大概率 seed_step,因为此时种子池已更丰富)——
这与领域语义一致,前端文案如实说明:"重试的点将由调度器自动选择冷启动或种子步进"。
#### ⑤ 列表接口内联计数(G2,卡片进度条用)
`GET /api/workflows``WorkflowSummary` 追加:
```json
{"name": "...", "status": "...", "description": "...",
"created_at": "...", "updated_at": "...",
"stats": {"total": 432, "converged": 261, "failed": 3, "running": 8,
"cold_run_converged": 220, "seed_step_converged": 41}}
```
- 实现:`list_workflows` 里对 `grid_points` 做一次 `GROUP BY workflow_name` 聚合再 map 合并(**单条 SQL,无 N+1**)。
- 未启动(无网格点)的工作流 `stats: null`,前端不显示进度条。
### 3.2 持久化变更(Phase 3,最小迁移)
> Phase 1/2 **零迁移**——全部基于现有列。以下为 Phase 3 的可选增强:
1. `grid_points` 加列:`elapsed_sec REAL`(最近一次成功/失败尝试的墙钟耗时)。
`record_task_report` 写入——`TaskReport.elapsed_sec` 本来就在手(`models.rs:318`),当前被丢弃。
2. `StageSummary` 携带 `last_iter / worst_depth / n_depths``models.rs:364-373` 扩字段,
`runner.rs:302-317``ConvCheckResult` 已有值,只是没搬过去)→ conv.json 与点详情获得迭代数。
3. 新表 `workflow_progress_snapshots(workflow_name TEXT, ts DATETIME, pending INT, queued INT,
running INT, converged INT, failed INT)`:由 `main.rs:111-197` 的 30s 后台循环顺手写一行;
保留策略:仅当计数相对上次快照有变化才写;定期清理 24h 前的行。供"进度-时间"曲线与精确 ETA。
### 3.3 明确不做 / 保持现状
- **不**改 pull 调度模型、不改 wave 语义、不改种子匹配算法——只读取与展示。
- **不**引入 WebSocket/SSE5s 轮询 + 详情弹窗作用域轮询足够,复杂度与收益不匹配。
- **不**给 `/api/seed/:name` 开 Admin 权限(它是 Node 角色的二进制通道);前端只需 conv.json 的 JSON 内容,走 ③ 即可。
- `seeds` 表/`.7` 文件不暴露列表下载(超出本次范围,且属 Node 域)。
### 3.4 安全清单(对照全局安全规约)
- 所有新端点经现有 `auth_middleware`(Admin 角色)+ 全局限流;无新增鉴权旁路。
- 点名/工作流名全部过字符集白名单(`[A-Za-z0-9._-]`);conv.json 读盘路径不接受 `..`/绝对路径。
- 无新增密钥;错误信息用现有 `AppError`Internal 不回显细节,`error.rs:28-34`)。
- 前端所有服务端字符串经 `escapeHtml`(列表/表格)或 `textContent`conv.json、错误文本)渲染。
- retry 端点为写操作:仅 Admin、受限于 running 状态、参数白名单,无注入面(参数化查询)。
---
## 4. 前端实现设计
### 4.1 新增/改动文件
| 文件 | 改动 |
|---|---|
| `src/router.js`(新) | hash 路由:`parseHash()` → `{view:'home''workflow', name?}``hashchange` 监听 + 首载渲染;视图挂载/卸载生命周期钩子(停/启作用域轮询);登录门禁判断 |
| `src/views/home.js`(新) | 现 `index.html` 中 `main.main-container` 的首页内容收敛为模板函数 + 挂载逻辑(metric 卡/节点表/工作流卡列表),`startPolling()` 在此触发 |
| `src/views/workflowDetail.js`(新) | 详情页:任务控制条(分段进度条/计数/ETA/操作)+ Tab 容器;概览(tiles/方法归因/wave 分布/最近动态流)、点表格(关键行签名 diff,仿 nodesTable)、过滤器、热力矩阵(CSS grid 单元格)、点详情滑入面板。所有插值 `escapeHtml`;作用域 5s 轮询自管 |
| `src/api.js` | 新增 `fetchWorkflowStatsApi(name)`、`fetchWorkflowPointsApi(name, params)`、`fetchPointDetailApi(name, point)`、`retryPointsApi(name, body)`;全部经 `apiFetch` + `encodeURIComponent`,返回 raw Response |
| `src/components/workflows.js` | 卡片加进度条/计数/状态徽章扩充;头部包成进详情的锚链接;读取列表内联 `stats`;空 stats 降级 |
| `src/main.js` | 瘦身为全局接线:登录/主题/登出/全局模态(create-wf / view-yaml / reissue / confirm/ header 刷新;视图级委托移入各 view |
| `src/state.js` | 全局轮询仅服务首页;视图切换时由 router 停启 |
| `index.html` | `main` 收敛为 `<div id="view-root">`(视图由 JS 渲染);保留登录遮罩/header/footer/全局模态/toast;新增 `#point-panel` 滑入面板骨架 |
| `src/style.css` | 新组件:`.wf-status-strip`(控制条 + 分段进度条 `.seg-progress` 五色段)、`.wf-tabs`tab 栏,双主题)、`.method-badge.cold/.seed/.imported`(蓝/紫/中性,**区别于状态色**)、`.heatmap`/`.heatmap-cell`(状态色填充 + hover)、`.stage-chip`(阶段诊断小卡)、`.point-panel`(右侧滑入 + 遮罩)、`.activity-feed`(动态流条目淡入);全部走设计令牌 + `prefers-reduced-motion` 降级 |
### 4.2 必须遵循的现有约定(探查结论)
1. 事件一律 `data-*` 委托,不挂逐按钮监听(`main.js:377-388` 模式)。
2. 服务端字符串进 `innerHTML` 前必过 `escapeHtml`;整块文本用 `textContent`。
3. 模态:静态骨架 + `setupFocusTrap`close 按钮类 `modal-close`(否则 Escape 映射失效,`modal.js:21`)。
4. 破坏性操作先 `showConfirm`,反馈只走 `showToast`,变更后 `fetchAllData()` 立即刷新。
5. 轮询下渲染:行/卡 keyed by `data-*` + 签名 diff + 高频单元格 `textContent` 原位刷新(`nodesTable.js:34-54` 模式),空态写入做 trim 比较防抖。
6. 样式只用令牌;状态复用 `.status-badge` 变体;进度复用 `.progress-bar-track/-fill`;表格复用 `.data-table` + `.col-*` + `.tabular-num`。
7. 工作流标识是 `name`(无数字 id);后端状态含 `initializing`,前端要显式映射。
---
## 5. 分期实施计划
### Phase 1 — 进度可见(MVP,零迁移)
- 后端:`stats` 端点(拆 queued+ `points` 端点(JOIN 最近尝试 + 新索引)+ 列表内联 `stats`。
- 前端:**hash 路由 + 视图化重构** + 卡片进度条/计数/徽章 + 工作流详情页(任务控制条 + 「概览」「网格点明细」两 Tab + 过滤器/分页 + 最近动态流)。
- 验收:深链 `#/workflows/sdB_cno` 打开详情页,能看到 432 点逐行状态、每点方法/max_relc/尝试数/种子来源,进度条随轮询而动。
### Phase 2 — 诊断与归因
- 后端:`points/:point` 端点(尝试历史 + conv.json 解析)+ `retry` 端点 + db `reset_failed_points`。
- 前端:点详情滑入面板(尝试历史 + 阶段链诊断 + 错误)+「收敛性分析」热力 Tab + 「重试失败点」按钮。
- 验收:能回答"这个点为什么失败/被谁救回",能看到冷启动成功参数区,能一键重试失败点。
### Phase 3 — 时序与精算(可选增强)
- 后端:迁移(`elapsed_sec` 列、`StageSummary.last_iter`+ `workflow_progress_snapshots` + ETA 精算。
- 前端:概览 ETA 精确化 + 进度-时间 sparkline + 点表耗时列去"~"。
---
## 6. 待确认的开放问题
1. Phase 范围:三期全做,还是先交付 Phase 1+2?
2. 「重试失败点」是否需要(它会触发真实计算任务)?默认仅 running 时可用。
3. 热力矩阵默认轴 Teff × cno_sum 是否符合你的物理直觉?(sdB_cno 当前网格 logg/logHe/CNO 各只有单值,实际是 Teff 一维——多值网格下矩阵才有意义。)
4. conv.json 之外是否还需要在点详情里提供逐阶段 `.err`/`fort.9` 原始文件下载(目前节点归档 `data/result` 有 LRU 200 上限,服务端只有 conv.json)?
</content>