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 笔误
This commit is contained in:
fmq
2026-08-04 23:40:52 +08:00
parent c8fd24b120
commit cd370d88e7
74 changed files with 13386 additions and 3033 deletions
+67 -54
View File
@@ -1,6 +1,60 @@
/* DCTS Dashboard Workflows Component */
import { escapeHtml } from '../api.js';
import { escapeHtml } from '../utils/format.js';
import { ICONS } from '../utils/icons.js';
/** 工作流卡片骨架:纯展示卡(名称/描述/状态/进度)。
*
* 卡片下方的「YAML配置/启动/暂停/删除」按钮已移除(见 docs/task_engine_decoupling_design.md
* §6.1)。所有操作(启动/停止/删除/保存配置)收敛至工作流详情页顶部的内嵌阶段配置面板
* wfEnginePanel.js)。首页卡片仅作状态概览与入口。 */
function buildCardSkeleton({ name, href, desc, statusCn, statusClass, hasProgress, pct, countsText }) {
return `
<a class="wf-card-link" href="${href}" title="进入工作流详情">
<div class="workflow-header">
<div>
<h3 class="workflow-name">${name}</h3>
<p class="workflow-desc">${desc}</p>
</div>
<span class="status-badge ${statusClass}" data-wf-badge>${statusCn}</span>
</div>
<div class="wf-card-progress${hasProgress ? '' : ' hidden'}" data-wf-progress>
<div class="progress-bar-track" aria-hidden="true">
<div class="progress-bar-fill progress-fill-emerald" data-wf-fill style="width: ${pct}%"></div>
</div>
<span class="wf-card-counts tabular-num" data-wf-counts>${countsText}</span>
</div>
</a>
`;
}
/** 原位更新卡片易变部分:状态徽章 / 进度条宽度 / 计数文本。 */
function updateCardInPlace(card, { statusCn, statusClass, hasProgress, pct, countsText }) {
// 状态徽章
const badge = card.querySelector('[data-wf-badge]');
if (badge) {
if (badge.textContent !== statusCn) badge.textContent = statusCn;
if (!badge.classList.contains(statusClass)) {
badge.className = `status-badge ${statusClass}`;
badge.setAttribute('data-wf-badge', '');
}
}
// 进度条
const progWrap = card.querySelector('[data-wf-progress]');
if (progWrap) {
const shouldShow = hasProgress;
const isHidden = progWrap.classList.contains('hidden');
if (shouldShow && isHidden) progWrap.classList.remove('hidden');
else if (!shouldShow && !isHidden) progWrap.classList.add('hidden');
if (shouldShow) {
const fill = progWrap.querySelector('[data-wf-fill]');
const wStr = `${pct}%`;
if (fill && fill.style.width !== wStr) fill.style.width = wStr;
const counts = progWrap.querySelector('[data-wf-counts]');
if (counts && counts.textContent !== countsText) counts.textContent = countsText;
}
}
}
export function renderWorkflowsSkeleton() {
const container = document.getElementById('workflows-list');
@@ -15,9 +69,8 @@ export function renderWorkflowsSkeleton() {
</div>
<div class="skeleton-shimmer skeleton-badge"></div>
</div>
<div class="workflow-actions wf-actions-mt">
<div class="skeleton-shimmer skeleton-btn"></div>
<div class="skeleton-shimmer skeleton-btn"></div>
<div class="wf-card-progress">
<div class="skeleton-shimmer skeleton-w-full skeleton-h-8"></div>
</div>
</div>
`).join('');
@@ -35,14 +88,10 @@ export function renderWorkflows(workflows) {
if (wfSub) wfSub.textContent = '暂无已注册工作流';
const emptyHtml = `
<div class="empty-state-box">
<svg class="empty-state-icon" width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<polygon points="12 2 2 7 12 12 22 7 12 2"/>
<polyline points="2 17 12 22 22 17"/>
<polyline points="2 12 12 17 22 12"/>
</svg>
${ICONS.layers({ size: 36, cls: 'empty-state-icon' })}
<p>暂无恒星大气网格工作流配置</p>
<button type="button" class="btn btn-primary btn-sm" data-wf-action="create-first">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
${ICONS.plus({ size: 14 })}
注册第一个工作流
</button>
</div>
@@ -91,61 +140,25 @@ export function renderWorkflows(workflows) {
const name = escapeHtml(wf.name);
const href = `#/workflows/${encodeURIComponent(wf.name)}`;
const desc = escapeHtml(wf.description || '无描述');
const isRunning = wf.status === 'running';
// 内联进度(来自列表接口内联 stats;未启动的工作流 stats=null → 不渲染)
const s = wf.stats;
let progressHtml = '';
if (s && s.total > 0) {
const pct = Math.min(100, Math.max(0, Math.round((s.converged / s.total) * 100)));
progressHtml = `
<div class="wf-card-progress">
<div class="progress-bar-track" aria-hidden="true">
<div class="progress-bar-fill progress-fill-emerald" style="width: ${pct}%"></div>
</div>
<span class="wf-card-counts tabular-num">${s.converged}/${s.total} 收敛 · ${s.seed_step_converged} 种子步进 · ${s.failed} 失败 · ${s.running} 运行</span>
</div>`;
}
const cardInnerHtml = `
<a class="wf-card-link" href="${href}" title="进入工作流详情">
<div class="workflow-header">
<div>
<h3 class="workflow-name">${name}</h3>
<p class="workflow-desc">${desc}</p>
</div>
<span class="status-badge ${statusClass}">${statusCn}</span>
</div>
${progressHtml}
</a>
<div class="workflow-actions">
<button class="btn btn-secondary btn-sm" data-wf-action="view" data-wf-name="${name}">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><path d="m16 13-2 2 2 2"/><path d="m8 13 2 2-2 2"/></svg>
YAML 配置
</button>
${
isRunning
? `<button class="btn btn-secondary btn-sm" data-wf-action="stop" data-wf-name="${name}"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg> 暂停</button>`
: `<button class="btn btn-primary btn-sm" data-wf-action="start" data-wf-name="${name}"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"/></svg> 启动</button>`
}
<button class="btn btn-danger btn-sm" data-wf-action="delete" data-wf-name="${name}">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
删除
</button>
</div>
`;
const hasProgress = s && s.total > 0;
const pct = hasProgress ? Math.min(100, Math.max(0, Math.round((s.converged / s.total) * 100))) : 0;
const countsText = hasProgress
? `${s.converged}/${s.total} 收敛 · ${s.seed_step_converged} 种子步进 · ${s.failed} 失败 · ${s.running} 运行`
: '';
let card = existingCards.get(name);
if (card) {
if (card.innerHTML.trim() !== cardInnerHtml.trim()) {
card.innerHTML = cardInnerHtml;
}
// 原位更新:状态徽章 / 进度条 / 计数文本——各自 diff,避免整卡 innerHTML 重建
updateCardInPlace(card, { statusCn, statusClass, hasProgress, pct, countsText });
existingCards.delete(name);
} else {
card = document.createElement('div');
card.className = 'workflow-card';
card.setAttribute('data-wf-card-name', name);
card.innerHTML = cardInnerHtml;
card.innerHTML = buildCardSkeleton({ name, href, desc, statusCn, statusClass, hasProgress, pct, countsText });
}
updatedCards.push(card);
});