Files
DCTS/docs/workflow_detail_design.md
T
fmq 43b82b1ae2 feat(all): 物理正确性五重硬门槛、输入文件结构化与 fort.55 错位修复、conv 诊断 DB 化与阶段归因修复、ORELAX 收敛修复与导入工具下线
物理正确性校验体系(common/conv_check.rs +494 行)
- 新增 5 类硬门槛:能量守恒(.6)、温度结构(.7)、emflux 积分校验(.emflux,含全 NaN 判失败)、假收敛排查(itek 轨迹首末比)、b 因子合理性(.bfac)
- runner 在 TLUSTY 阶段结束后执行全部校验,任一失败判 final_converged=false
- GridConfig 新增 8 个可配阈值,经 scheduler→executor→runner 全链路透传

输入文件配置结构化重构(config.rs +1453 行)
- TlustyInput 拆为 dot5/nst 分层结构,字段名严格映射 tlusty208.f READ 语句;SynspecInput 重构为 9 个 Fort55Line 子结构体
- 移除 ChainStep.metals 字段,元素集改由 dot5.atoms/ions 显式声明(gen_input5/nst_writer 同步重写为三源融合 / 分层覆盖)
- fort.55 修复行结构 bug:补全分子表行(7→9 行),IDSTD 50→0 错位修正(影响全部光谱线强归一化,需重算 SYNSPEC 阶段)

conv 诊断 DB 化与阶段归因修复(server)
- 单点详情 conv 面板从磁盘 conv.json 改读 DB grid_points.summary_json;grid_points 新增 summary_json/last_elapsed_sec 两列(旧库幂等 ALTER)
- record_task_report 阶段归因列加 CASE 守卫 + clear_synspec 对称处理,修复 synspec-only/TLUSTY-only 重跑污染统计
- 新增 summary_merge.rs 点级增量合并,避免重跑覆盖诊断字段

收敛性 ORELAX 修复与 seed_chain 可配(sdB_cno.yaml + node)
- nl 阶段加 orelax=0.5、seed_nc 加 orelax=0.3,阻尼中温区 relc 振荡发散
- seed_chain 块可配,executor 优先采用用户配置而非内置默认链

导入工具下线
- 删除 import_results 客户端工具及 Windows 推送脚本;移除 /admin/import_seed 端点
- 改为服务端临时 migrate_conv 端点(扫 conv.json 增量合并入库,迁移后可删)

文档与分析
- 新增 1305 失败点根因分析、fort.14 全 NaN 物理含义分析两份深度文档
- spectrum_correctness_analysis 两次修订标注已修复项;fetch_results.sh 修 trap RETURN 的 set -u 报错
2026-08-09 12:09:48 +08:00

408 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 统计),
> 大气方法过滤白名单 `cold_run` / `seed_step`(映射 `tlusty_success_method`);另有 `synspec_only` 光谱专用点过滤器(`tlusty IS NULL AND synspec IS NOT NULL`)。
---
## 1. 现状盘点(带代码证据)
### 1.1 已经持久化、可直接用的数据
| 数据 | 位置 | 说明 |
|---|---|---|
| 网格点 6 维参数、`cno_sum``wave` | `grid_points` 表(`db.rs:246-263` | 按 `workflow_name` 分区,复合唯一 `(workflow_name, name)` |
| 点状态 `pending/queued/running/completed/failed``completed` 由 7c 改名自 `converged` | `grid_points.status` | `GridPointStatus``models.rs:235-243` |
| **大气成功手段** `cold_run` / `seed_step` / `imported` | `grid_points.tlusty_success_method` | TLUSTY 阶段收敛时由成功任务的 `tlusty_strategies[0]` 派生回填(Phase 6 起 `task_type` 列已删除);TLUSTY 禁用(synspec-only)为 NULL。光谱阶段另存 `synspec_success_method`Phase 5a,取 `synspec_strategies[0]`)。整体归因由消费方派生 `tlusty ?? synspec`P9 拆分,原 `success_method` 值域混用列已删) |
| 重试次数 | `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.tlusty_strategies[0] / error_message / node_id / completed_at` | 一个点多行(每次尝试一行),工作流删除前长期保留;方法取自策略链首项(`task_type` 列已删) |
| 全局种子库 | `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``StepSummary` 前被丢弃) | 列表无耗时列、无 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` 一次性闸门约束)。
因此 `tlusty_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, "completed": 261, "failed": 3,
"cold_run_converged": 220, "seed_step_converged": 41,
"waves": [ {"wave": 0, "total": 108, "completed": 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": "completed", "tlusty_success_method": "seed_step", "synspec_success_method": "standard", "attempt_count": 2,
"last_max_relc": 0.00043,
"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",
"last_elapsed_sec": 126.4
}
]
}
}
```
- "最近一次尝试"用关联子查询取 `tasks` 最新行:
```sql
SELECT gp.*, t.max_relc AS last_max_relc,
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": "...", "seed_point_name": null, // Phase 6 起无 task_type 字段
"status": "failed", "max_relc": 954000.0, "atmosphere_has_nan": true,
"node_id": "node-a1b2", "error_message": "nl stage diverged...",
"failed_stage": "tlusty", "summary_json": "{...}",
"created_at": "...", "completed_at": "...", "elapsed_sec": 1800.0},
{"task_id": "...", "seed_point_name": "t60000_...", // 第二次尝试(seed_step 热启动救回)
"status": "completed", "max_relc": 0.00043, "failed_stage": null, ...}
],
"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', tlusty_success_method=NULL, synspec_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, "completed": 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. `StepSummary` 携带 `last_iter / worst_depth / n_depths``models.rs:590-612` 扩字段,
`runner.rs:302-317``ConvCheckResult` 已有值,只是没搬过去)→ conv.json 与点详情获得迭代数。
3. 新表 `workflow_progress_snapshots(workflow_name TEXT, ts DATETIME, pending INT, queued INT,
running INT, completed 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` 列、`StepSummary.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>