feat(all): NaN 伪收敛否决与 nl_tight 能量回退、emflux 检验 4π 修正、nst 行宽与种子边界修复、gfATO 谱线表接通与网格加密 9216 点、阶段分项统计与跳板机部署
物理修复(400 失败点归因,见 docs/failed400_nan_pseudo_convergence_2026_08_17.md): - runner: 阶段 converged 后复查 fort.7,含 NaN/Inf 即否决(fort.9 全零伪收敛, 曾致 261 点误跳过 nl_direct 回退);否决阶段不产出种子,阻断污染传播 - runner: nl_tight 回退——仅能量边际失败时以 CHMAX 收紧 10× 从自身模型续迭代, 残差降幅 ~10×;execute_tlusty_stage 抽取供主链与回退共用 - conv_check: fort.14 为 Eddington 通量 Hλ,积分需乘 4π 再比 σTeff⁴ (旧版 ratio 稳定 0.0796=1/4π,全点系统性假阳性)+ 回归测试 - nst_writer: 单行超 80 字符被 TLUSTY 静默截断,IFALI/JALI/TRAD 等从未生效; 按 75 字符自动换行 - seed_finder: Teff 容忍度改含边界 <=,相邻 5000K 档恢复互为种子 + 回归测试 谱线表与网格: - 默认线表 gfVIS99 → gfATO(全波段 18-23000Å),TaskSpec.linelist 支持工作流 级覆盖,节点按需下载(进程互斥锁防并发重复下载 238MB) - sdB_cno Teff 加密至 5000K 步长,432 → 9216 点;tlusty/synspec 静态二进制更新 统计与部署: - grid 汇总改按 tlusty_status/synspec_status 分项计数,新增 tlusty_failed/ synspec_failed/synspec_pending,前端详情页双视图适配 - deploy/fetch_results 支持跳板机 ProxyJump 与 SSH 主连接复用,fetch 新增 --force; - Docker 构建支持 CARGO_MIRROR/USE_MIRRORS 国内镜像参数;移除 tools/ 拷贝 - 新增 tlusty-synspec-test skill 与 6 篇根因分析/验证文档
This commit is contained in:
@@ -67,6 +67,12 @@ const TOKEN_KEY = 'dcts_admin_token';
|
||||
* @property {number} running
|
||||
* @property {number} completed
|
||||
* @property {number} failed
|
||||
* @property {number} cold_run_converged - TLUSTY 视图:冷启动收敛
|
||||
* @property {number} seed_step_converged - TLUSTY 视图:种子步进收敛
|
||||
* @property {number} tlusty_failed - TLUSTY 视图:大气发散
|
||||
* @property {number} synspec_converged - SYNSPEC 视图:光谱有效
|
||||
* @property {number} synspec_failed - SYNSPEC 视图:光谱失败
|
||||
* @property {number} synspec_pending - SYNSPEC 视图:未运行
|
||||
* @property {number} [eta_sec]
|
||||
* @property {Array<{label:string,count:number}>} [waves]
|
||||
*/
|
||||
@@ -76,6 +82,8 @@ const TOKEN_KEY = 'dcts_admin_token';
|
||||
* @property {string} name
|
||||
* @property {string} status
|
||||
* @property {string} [method]
|
||||
* @property {string} [tlusty_status] - 'converged'/'failed'/'pending'/null
|
||||
* @property {string} [synspec_status] - 'converged'/'failed'/'pending'/null
|
||||
* @property {number} [relc]
|
||||
* @property {number} [runtime_sec]
|
||||
* @property {number} [attempts]
|
||||
|
||||
@@ -1043,6 +1043,8 @@ body::before {
|
||||
}
|
||||
|
||||
.seg-converged { background: var(--color-success); }
|
||||
/* 种子步进段(TLUSTY 视图):紫色,与冷启动绿区分收敛策略。 */
|
||||
.seg-seed { background: var(--accent-purple); }
|
||||
/* 色盲友好(color-not-only):失败段在红色之上叠加斜线纹理,
|
||||
即使无法分辨红/绿也能凭纹理识别失败占比。 */
|
||||
.seg-failed {
|
||||
@@ -1056,6 +1058,39 @@ body::before {
|
||||
.seg-queued { background: var(--color-info); }
|
||||
.seg-pending { background: var(--border-default); }
|
||||
|
||||
/* 阶段切换器(TLUSTY/SYNSPEC 双视图)+ 计数行布局 */
|
||||
.wf-strip-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.phase-switcher {
|
||||
display: inline-flex;
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
}
|
||||
.phase-tab {
|
||||
appearance: none;
|
||||
border: none;
|
||||
padding: var(--space-1) var(--space-3);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
background: var(--bg-surface);
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
.phase-tab + .phase-tab { border-left: 1px solid var(--border-default); }
|
||||
.phase-tab:hover { background: var(--bg-subtle); }
|
||||
.phase-tab.active {
|
||||
color: var(--color-success);
|
||||
background: var(--color-success-bg);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.wf-strip-counts {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
@@ -1155,6 +1190,35 @@ body::before {
|
||||
border-color: var(--color-danger);
|
||||
}
|
||||
|
||||
/* 阶段状态徽标(单点详情面板顶部):TLUSTY ✓/✗ · SYNSPEC ✓/✗
|
||||
半失败点显示「整体失败 · TLUSTY ✓ · SYNSPEC ✗」,让阶段级成败一眼可见。 */
|
||||
.stage-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 1px var(--space-2);
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid transparent;
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.stage-badge.stage-ok {
|
||||
color: var(--color-success);
|
||||
background: var(--color-success-bg);
|
||||
border-color: var(--color-success);
|
||||
}
|
||||
.stage-badge.stage-fail {
|
||||
color: var(--color-danger);
|
||||
background: var(--color-danger-bg);
|
||||
border-color: var(--color-danger);
|
||||
}
|
||||
.stage-badge.stage-pending {
|
||||
color: var(--text-muted);
|
||||
background: var(--bg-subtle);
|
||||
border-color: var(--border-default);
|
||||
}
|
||||
|
||||
/* 状态徽章补充变体(失败=红 / 排队=蓝) */
|
||||
.status-badge.danger {
|
||||
color: var(--color-danger);
|
||||
@@ -1376,6 +1440,7 @@ body::before {
|
||||
.ps-cold { fill: var(--color-success); }
|
||||
.ps-seed { fill: var(--accent-purple); }
|
||||
.ps-synspec { fill: var(--accent-teal); }
|
||||
.ps-converged { fill: var(--color-success); }
|
||||
.ps-failed { fill: var(--color-danger); }
|
||||
.ps-running { fill: var(--color-warning); }
|
||||
.ps-queued { fill: var(--color-info); }
|
||||
@@ -1384,6 +1449,7 @@ body::before {
|
||||
.ps-seg.ps-cold { fill: var(--color-success); }
|
||||
.ps-seg.ps-seed { fill: var(--accent-purple); }
|
||||
.ps-seg.ps-synspec { fill: var(--accent-teal); }
|
||||
.ps-seg.ps-converged { fill: var(--color-success); }
|
||||
.ps-seg.ps-failed { fill: var(--color-danger); }
|
||||
.ps-seg.ps-running { fill: var(--color-warning); }
|
||||
.ps-seg.ps-queued { fill: var(--color-info); }
|
||||
@@ -1404,6 +1470,7 @@ body::before {
|
||||
.ps-vlabel-cold { fill: var(--color-success); font-weight: 600; }
|
||||
.ps-vlabel-seed { fill: var(--accent-purple); font-weight: 600; }
|
||||
.ps-vlabel-synspec { fill: var(--accent-teal); font-weight: 600; }
|
||||
.ps-vlabel-converged { fill: var(--color-success); font-weight: 600; }
|
||||
.ps-vlabel-failed { fill: var(--color-danger); font-weight: 600; }
|
||||
.ps-vlabel-running { fill: var(--color-warning); font-weight: 600; }
|
||||
.ps-vlabel-queued { fill: var(--color-info); font-weight: 600; }
|
||||
|
||||
@@ -24,6 +24,9 @@ export function createDetailCtx(name) {
|
||||
|
||||
// ===== Tab1 执行概览 =====
|
||||
activeTab: 'overview',
|
||||
// 阶段视图切换('tlusty' 大气 / 'synspec' 光谱):概览分段进度条/指标卡 + parSets 状态轴共用。
|
||||
// 切换按钮在详情页顶部 strip,默认 TLUSTY 视图。
|
||||
phaseView: 'tlusty',
|
||||
latestStats: null,
|
||||
overviewBuilt: false, // 概览骨架仅首次构建,其后轮询原位 diff
|
||||
lastWavesSig: null,
|
||||
|
||||
@@ -14,30 +14,57 @@ import { fetchWorkflowPointsApi, fetchWorkflowProgressApi } from '../../api.js';
|
||||
import { isAbortError, logError } from '../../utils/errors.js';
|
||||
import { ICONS } from '../../utils/icons.js';
|
||||
|
||||
/** 分段进度条档位:顺序与计数行一致。五段常驻(空档宽度 0),原位改 width 复用 CSS 过渡。 */
|
||||
const SEG_DEFS = [
|
||||
['completed', 'seg-converged', '完成'],
|
||||
['failed', 'seg-failed', '失败'],
|
||||
/** 分段进度条档位定义——TLUSTY/SYNSPEC 双视图各一套(按 ctx.phaseView 切换)。
|
||||
* TLUSTY 视图:cold/seed(大气收敛,按策略拆)+ failed(大气发散)+ running/queued/pending。
|
||||
* SYNSPEC 视图:converged(光谱有效)+ failed(光谱坏)+ pending(未运行)。 */
|
||||
const SEG_DEFS_TLUSTY = [
|
||||
['cold', 'seg-converged', '冷启动收敛'],
|
||||
['seed', 'seg-seed', '种子步进'],
|
||||
['failed', 'seg-failed', '大气发散'],
|
||||
['running', 'seg-running', '运行'],
|
||||
['queued', 'seg-queued', '排队'],
|
||||
['pending', 'seg-pending', '待定'],
|
||||
];
|
||||
const SEG_DEFS_SYNSPEC = [
|
||||
['converged', 'seg-converged', '光谱有效'],
|
||||
['failed', 'seg-failed', '光谱失败'],
|
||||
['pending', 'seg-pending', '未运行'],
|
||||
];
|
||||
|
||||
/** 任务控制条:状态徽章 / 分段进度条 / 计数与 ETA。
|
||||
*
|
||||
* 操作按钮(启动/暂停/删除/查看)已移除——全部收敛至内嵌引擎面板
|
||||
* (wfEnginePanel.js,见 docs/task_engine_decoupling_design.md §6)。
|
||||
* 剩余元素签名 diff 后原位更新,轮询不重建 DOM。 */
|
||||
export function renderStrip(s) {
|
||||
/** 选中阶段的档位定义。stats 的字段名映射:cold←cold_run_converged, seed←seed_step_converged,
|
||||
* failed←(tlusty_failed|synspec_failed), converged←synspec_converged, pending←(pending|synspec_pending)。 */
|
||||
function segDefsFor(phase) {
|
||||
return phase === 'synspec' ? SEG_DEFS_SYNSPEC : SEG_DEFS_TLUSTY;
|
||||
}
|
||||
|
||||
/** 把 stats 字段值映射到档位计数。phase 决定 failed/pending 取哪个字段。 */
|
||||
function segCount(s, key, phase) {
|
||||
if (key === 'cold') return s.cold_run_converged || 0;
|
||||
if (key === 'seed') return s.seed_step_converged || 0;
|
||||
if (key === 'converged') return s.synspec_converged || 0;
|
||||
if (key === 'failed') return phase === 'synspec' ? (s.synspec_failed || 0) : (s.tlusty_failed || 0);
|
||||
if (key === 'pending') return phase === 'synspec' ? (s.synspec_pending || 0) : (s.pending || 0);
|
||||
if (key === 'running') return s.running || 0;
|
||||
if (key === 'queued') return s.queued || 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** 任务控制条:状态徽章 / 阶段切换器 / 分段进度条 / 计数与 ETA。
|
||||
* phase 参数('tlusty'|'synspec')决定分段进度条与计数行用哪套阶段档位。 */
|
||||
export function renderStrip(s, phase = 'tlusty') {
|
||||
const statusEl = document.getElementById('wf-detail-status');
|
||||
if (statusEl && statusEl.getAttribute('data-status') !== s.status) {
|
||||
statusEl.setAttribute('data-status', s.status);
|
||||
statusEl.innerHTML = statusBadge(s.status);
|
||||
}
|
||||
|
||||
const SEG_DEFS = segDefsFor(phase);
|
||||
const segEl = document.getElementById('wf-seg-progress');
|
||||
if (segEl) {
|
||||
if (!segEl.firstElementChild) {
|
||||
// 档位定义随阶段切换变化 → 重建分段(切换阶段时 sig 变化触发重建,同阶段轮询复用)。
|
||||
const sig = SEG_DEFS.map(d => d[0]).join(',');
|
||||
if (segEl.getAttribute('data-seg-sig') !== sig) {
|
||||
segEl.setAttribute('data-seg-sig', sig);
|
||||
segEl.innerHTML = SEG_DEFS.map(([key, cls, label]) =>
|
||||
`<div class="seg ${cls}" data-seg="${key}" style="width:0%" title="${label} 0"></div>`).join('');
|
||||
}
|
||||
@@ -45,8 +72,7 @@ export function renderStrip(s) {
|
||||
SEG_DEFS.forEach(([key, , label]) => {
|
||||
const el = segEl.querySelector(`[data-seg="${key}"]`);
|
||||
if (!el) return;
|
||||
const n = s[key] || 0;
|
||||
// total=0 时 pending 占满整条,沿用旧版「暂无网格点」语义
|
||||
const n = segCount(s, key, phase);
|
||||
const w = total > 0 ? (n / total) * 100 : (key === 'pending' ? 100 : 0);
|
||||
const wStr = `${w}%`;
|
||||
if (el.style.width !== wStr) el.style.width = wStr;
|
||||
@@ -57,15 +83,20 @@ export function renderStrip(s) {
|
||||
|
||||
const countsEl = document.getElementById('wf-strip-counts');
|
||||
if (countsEl) {
|
||||
const pct = s.total ? Math.round((s.completed / s.total) * 100) : 0;
|
||||
const parts = [
|
||||
`${s.total} 网格点`,
|
||||
`${s.completed} 完成${s.total ? ` (${pct}%)` : ''}`,
|
||||
`${s.running} 运行`,
|
||||
`${s.queued} 排队`,
|
||||
`${s.pending} 待定`,
|
||||
`${s.failed} 失败`,
|
||||
];
|
||||
const parts = [`${s.total} 网格点`];
|
||||
if (phase === 'synspec') {
|
||||
parts.push(`${s.synspec_converged || 0} 光谱有效`);
|
||||
parts.push(`${s.synspec_failed || 0} 光谱失败`);
|
||||
parts.push(`${s.synspec_pending || 0} 未运行`);
|
||||
} else {
|
||||
const conv = (s.cold_run_converged || 0) + (s.seed_step_converged || 0);
|
||||
const pct = s.total ? Math.round((conv / s.total) * 100) : 0;
|
||||
parts.push(`${conv} 大气收敛${s.total ? ` (${pct}%)` : ''}`);
|
||||
parts.push(`${s.tlusty_failed || 0} 大气发散`);
|
||||
parts.push(`${s.running} 运行`);
|
||||
if (s.queued) parts.push(`${s.queued} 排队`);
|
||||
if (s.pending) parts.push(`${s.pending} 待定`);
|
||||
}
|
||||
if (s.eta_sec != null) parts.push(`ETA ≈ ${fmtDuration(s.eta_sec)}`);
|
||||
const text = parts.join(' · ');
|
||||
if (countsEl.textContent !== text) countsEl.textContent = text;
|
||||
@@ -79,12 +110,46 @@ export function renderOverview(ctx, s) {
|
||||
if (!ctx.overviewBuilt) {
|
||||
ctx.overviewBuilt = true;
|
||||
panel.innerHTML = overviewSkeleton();
|
||||
bindPhaseSwitcher(ctx);
|
||||
}
|
||||
updateOverview(ctx, s);
|
||||
refreshActivityFeed(ctx);
|
||||
}
|
||||
|
||||
/** 概览骨架:动态部分全部挂 ID,供 updateOverview 原位更新。 */
|
||||
/** 阶段切换器事件绑定:TLUSTY/SYNSPEC 双视图。切换时重渲染 strip + overview + parSets。 */
|
||||
function bindPhaseSwitcher(ctx) {
|
||||
document.querySelectorAll('.phase-tab').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const phase = btn.getAttribute('data-phase');
|
||||
if (!phase || phase === ctx.phaseView) return;
|
||||
ctx.phaseView = phase;
|
||||
document.querySelectorAll('.phase-tab').forEach(b => {
|
||||
const active = b.getAttribute('data-phase') === phase;
|
||||
b.classList.toggle('active', active);
|
||||
b.setAttribute('aria-selected', String(active));
|
||||
});
|
||||
// 重渲染 strip(分段进度条 + 计数行)+ 概览指标卡
|
||||
if (ctx.latestStats) {
|
||||
renderStrip(ctx.latestStats, phase);
|
||||
updateOverview(ctx, ctx.latestStats);
|
||||
}
|
||||
// 重渲染 parSets(如果分析 Tab 已有数据)
|
||||
const container = document.getElementById('ps-container');
|
||||
if (container && ctx.psState?.points?.length > 0) {
|
||||
// 触发 parSets 重渲染(renderParSets 在 parSets.js 内部,经 renderAnalysisTab 间接调用)
|
||||
// 这里直接 import 会循环依赖,用事件委托:parSets 的 renderAnalysisTab 检测 phaseView 变化时重渲染。
|
||||
// 简化:切到 analysis Tab 时 renderAnalysisTab 会读 ctx.phaseView,已切换则用新档位。
|
||||
// 若当前已在 analysis Tab,手动触发一次渲染。
|
||||
if (ctx.activeTab === 'analysis') {
|
||||
window.dispatchEvent(new CustomEvent('dcts-phase-change', { detail: { phase } }));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** 概览骨架:动态部分全部挂 ID,供 updateOverview 原位更新。
|
||||
* 指标卡按阶段切换语义——"阶段收敛/失败"随 ctx.phaseView 变化。 */
|
||||
function overviewSkeleton() {
|
||||
return `
|
||||
<section class="metrics-grid">
|
||||
@@ -94,14 +159,14 @@ function overviewSkeleton() {
|
||||
<span class="metric-sub">6 维参数笛卡尔积展开</span>
|
||||
</div>
|
||||
<div class="metric-card card">
|
||||
<span class="metric-title">已完成</span>
|
||||
<span class="metric-title" id="ov-m-converged-title">大气收敛</span>
|
||||
<span class="metric-value tabular-num" id="ov-m-converged">—</span>
|
||||
<span class="metric-sub" id="ov-m-converged-sub">—</span>
|
||||
</div>
|
||||
<div class="metric-card card">
|
||||
<span class="metric-title">失败</span>
|
||||
<span class="metric-title" id="ov-m-failed-title">大气发散</span>
|
||||
<span class="metric-value tabular-num" id="ov-m-failed">—</span>
|
||||
<span class="metric-sub">冷启动发散且无种子可救</span>
|
||||
<span class="metric-sub" id="ov-m-failed-sub">—</span>
|
||||
</div>
|
||||
<div class="metric-card card">
|
||||
<span class="metric-title">运行 + 排队</span>
|
||||
@@ -142,17 +207,31 @@ function setOvText(id, v) {
|
||||
if (el && el.textContent !== str) el.textContent = str;
|
||||
}
|
||||
|
||||
/** 轮询原位刷新概览数值(指标卡 + 归因徽章 + 波次)。 */
|
||||
/** 轮询原位刷新概览数值(指标卡 + 波次)。按 ctx.phaseView 切换阶段语义。 */
|
||||
function updateOverview(ctx, s) {
|
||||
const pct = s.total ? Math.round((s.completed / s.total) * 100) : 0;
|
||||
const phase = ctx.phaseView || 'tlusty';
|
||||
setOvText('ov-m-total', s.total);
|
||||
setOvText('ov-m-converged', s.completed);
|
||||
setOvText('ov-m-converged-sub', `完成率 ${pct}%`);
|
||||
setOvText('ov-m-failed', s.failed);
|
||||
setOvText('ov-m-runqueue', s.running + s.queued);
|
||||
setOvText('ov-m-runqueue-sub', `${s.running} 运行 · ${s.queued} 排队`);
|
||||
// 收敛手段归因面板已移除(见 docs/task_engine_decoupling_design.md §6.1),
|
||||
// cold/seed 计数仍在点表明细与 parSets 中可用,概览不再单独展示。
|
||||
setOvText('ov-m-runqueue', (s.running || 0) + (s.queued || 0));
|
||||
setOvText('ov-m-runqueue-sub', `${s.running || 0} 运行 · ${s.queued || 0} 排队`);
|
||||
if (phase === 'synspec') {
|
||||
const conv = s.synspec_converged || 0;
|
||||
const pct = s.total ? Math.round((conv / s.total) * 100) : 0;
|
||||
setOvText('ov-m-converged-title', '光谱有效');
|
||||
setOvText('ov-m-converged', conv);
|
||||
setOvText('ov-m-converged-sub', `光谱成功率 ${pct}%`);
|
||||
setOvText('ov-m-failed-title', '光谱失败');
|
||||
setOvText('ov-m-failed', s.synspec_failed || 0);
|
||||
setOvText('ov-m-failed-sub', `未运行 ${s.synspec_pending || 0}`);
|
||||
} else {
|
||||
const conv = (s.cold_run_converged || 0) + (s.seed_step_converged || 0);
|
||||
const pct = s.total ? Math.round((conv / s.total) * 100) : 0;
|
||||
setOvText('ov-m-converged-title', '大气收敛');
|
||||
setOvText('ov-m-converged', conv);
|
||||
setOvText('ov-m-converged-sub', `冷启动 ${s.cold_run_converged || 0} · 种子步进 ${s.seed_step_converged || 0}(${pct}%)`);
|
||||
setOvText('ov-m-failed-title', '大气发散');
|
||||
setOvText('ov-m-failed', s.tlusty_failed || 0);
|
||||
setOvText('ov-m-failed-sub', '收敛阈值未达标且无种子可救');
|
||||
}
|
||||
updateWaves(ctx, s);
|
||||
}
|
||||
|
||||
|
||||
@@ -58,30 +58,45 @@ export function restorePsCache(ctx) {
|
||||
}
|
||||
}
|
||||
|
||||
/** 状态轴档位(收敛拆冷启动/种子步进/SYNSPEC,物理意义不同:稳定区 vs 救回区 vs 光谱重算)。
|
||||
/** 状态轴档位定义——TLUSTY/SYNSPEC 双视图各一套。
|
||||
* 顺序即图例/轴布局/每值内分段/色带绘制层叠的唯一来源,语义约定:
|
||||
* 已收敛(最受关注)置顶 → 进行中按任务进展方向(running→queued→pending)→ 失败沉底。
|
||||
* 色带绘制按此顺序:收敛色带先画(底层),失败最后画(最上层,数量少且颜色醒目)。
|
||||
* synspec 档:synspec_success_method 非空(如 synspec-only 任务的光谱策略 "standard"),
|
||||
* 属已收敛但由光谱合成阶段产出,需与大气收敛区分。 */
|
||||
const PS_STATUS = ['cold', 'seed', 'synspec', 'running', 'queued', 'pending', 'failed'];
|
||||
*
|
||||
* TLUSTY 视图(大气阶段):基于 tlusty_status + tlusty_success_method。
|
||||
* cold/seed = 大气收敛(按策略拆),failed = 大气发散,running/queued/pending = 未跑完。
|
||||
* SYNSPEC 视图(光谱阶段):基于 synspec_status。
|
||||
* converged = 光谱有效,failed = 光谱坏(Balmer edge NaN 等),pending = 未运行(多因 tlusty 未收敛阻塞)。 */
|
||||
const PS_STATUS_TLUSTY = ['cold', 'seed', 'failed', 'running', 'queued', 'pending'];
|
||||
const PS_STATUS_SYNSPEC = ['converged', 'failed', 'pending'];
|
||||
const PS_STATUS_LABEL = {
|
||||
cold: '冷启动收敛', seed: '种子步进', synspec: 'SYNSPEC 合成', failed: '失败',
|
||||
cold: '冷启动收敛', seed: '种子步进', failed: '失败',
|
||||
running: '运行', queued: '排队', pending: '未开始',
|
||||
converged: '光谱有效',
|
||||
};
|
||||
|
||||
/** 点 → 状态档位 key。
|
||||
* 阶段归因列拆分(P9)后可直接区分:TLUSTY 策略看 tlusty_success_method,
|
||||
* SYNSPEC-only 点看 synspec_success_method——不再需要猜策略名(原 SYNSPEC_METHODS hack)。 */
|
||||
function psSlot(p) {
|
||||
if (p.status === 'completed') {
|
||||
if (p.tlusty_success_method === 'seed_step') return 'seed';
|
||||
if (p.tlusty_success_method === 'cold_run') return 'cold';
|
||||
// 光谱阶段收敛(synspec-only/双阶段光谱归因落库)→ SYNSPEC 合成档。
|
||||
if (p.synspec_success_method) return 'synspec';
|
||||
return 'cold';
|
||||
/** 当前选中阶段的档位列表(renderParSets 读 ctx.phaseView 切换)。 */
|
||||
function psStatusList(phase) {
|
||||
return phase === 'synspec' ? PS_STATUS_SYNSPEC : PS_STATUS_TLUSTY;
|
||||
}
|
||||
|
||||
/** 点 → 状态档位 key(按选中阶段映射)。
|
||||
* TLUSTY 阶段:tlusty_status='converged' 按 tlusty_success_method 拆 cold/seed;'failed'→failed;
|
||||
* 否则按 grid_points.status 归 running/queued/pending。
|
||||
* SYNSPEC 阶段:synspec_status='converged'→converged;'failed'→failed;其余→pending(含 NULL)。 */
|
||||
function psSlot(p, phase) {
|
||||
if (phase === 'synspec') {
|
||||
const s = p.synspec_status;
|
||||
if (s === 'converged') return 'converged';
|
||||
if (s === 'failed') return 'failed';
|
||||
return 'pending'; // pending / NULL / 未知 → 未运行
|
||||
}
|
||||
if (p.status === 'failed') return 'failed';
|
||||
// TLUSTY 视图
|
||||
if (p.tlusty_status === 'converged') {
|
||||
if (p.tlusty_success_method === 'seed_step') return 'seed';
|
||||
return 'cold'; // cold_run 或缺省归冷启动
|
||||
}
|
||||
if (p.tlusty_status === 'failed') return 'failed';
|
||||
if (p.status === 'running') return 'running';
|
||||
if (p.status === 'queued') return 'queued';
|
||||
return 'pending';
|
||||
@@ -96,9 +111,29 @@ function psFmtDim(dim, v) {
|
||||
return n.toFixed(0); // loghe/logc/logn/logo 都是整数采样
|
||||
}
|
||||
|
||||
/** 卸载时清理 parSets 模块注册的 window 级监听器,防止页面离开后旧 ctx 闭包驻留内存
|
||||
* 并对游离 DOM 执行无效重绘(GC Leak)。由 workflowDetail.unmountWorkflowDetail 调用。 */
|
||||
export function cleanupParSetsListeners(ctx) {
|
||||
if (ctx?._psPhaseListener) {
|
||||
window.removeEventListener('dcts-phase-change', ctx._psPhaseListener);
|
||||
ctx._psPhaseListener = null;
|
||||
}
|
||||
}
|
||||
|
||||
export function renderAnalysisTab(ctx) {
|
||||
const el = document.getElementById('wf-tab-analysis');
|
||||
if (!el) return;
|
||||
// 阶段切换器事件:概览页的 TLUSTY/SYNSPEC 切换会派发 dcts-phase-change,
|
||||
// 若分析 Tab 已构建且已有数据,按新阶段重渲染平行集合图。
|
||||
if (!ctx._psPhaseListener) {
|
||||
ctx._psPhaseListener = (e) => {
|
||||
if (ctx.psBuilt && ctx.psState?.points?.length > 0) {
|
||||
renderParSets(ctx);
|
||||
renderPsConclusion(ctx, ctx.psState.points);
|
||||
}
|
||||
};
|
||||
window.addEventListener('dcts-phase-change', ctx._psPhaseListener);
|
||||
}
|
||||
if (!ctx.psBuilt) {
|
||||
ctx.psBuilt = true;
|
||||
el.innerHTML = `
|
||||
@@ -175,8 +210,10 @@ function renderParSets(ctx) {
|
||||
return;
|
||||
}
|
||||
|
||||
const phase = ctx.phaseView || 'tlusty';
|
||||
const PS_STATUS = psStatusList(phase);
|
||||
const dims = [...PS_DIMS, 'status'];
|
||||
// 每维取值列表(status 用 PS_STATUS 顺序)
|
||||
// 每维取值列表(status 用当前阶段的 PS_STATUS 顺序)
|
||||
const dimValues = {};
|
||||
PS_DIMS.forEach(d => { dimValues[d] = psDimValues(points, d); });
|
||||
dimValues.status = PS_STATUS;
|
||||
@@ -190,7 +227,7 @@ function renderParSets(ctx) {
|
||||
PS_DIMS.forEach(d => { ptNum[d] = new Float64Array(nPts); });
|
||||
for (let i = 0; i < nPts; i++) {
|
||||
const p = points[i];
|
||||
ptSlotArr[i] = psSlot(p);
|
||||
ptSlotArr[i] = psSlot(p, phase);
|
||||
for (const d of PS_DIMS) ptNum[d][i] = Number(p[d]);
|
||||
}
|
||||
|
||||
@@ -365,16 +402,17 @@ export function reflowParSets(ctx) {
|
||||
if (c && Math.abs((c.clientWidth || 0) - prev) > 20) renderParSets(ctx);
|
||||
}
|
||||
|
||||
/** 自动洞察:扫各维度取值,挑失败率显著高于均值的位置。 */
|
||||
/** 自动洞察:扫各维度取值,挑失败率显著高于均值的位置。按选中阶段用对应档位计数。 */
|
||||
function renderPsConclusion(ctx, pts) {
|
||||
const el = document.getElementById('ps-conclusion');
|
||||
if (!el) return;
|
||||
const total = pts.length;
|
||||
if (total === 0) { el.textContent = ''; return; }
|
||||
const cold = pts.filter(p => p.status === 'completed' && p.tlusty_success_method === 'cold_run').length;
|
||||
const seed = pts.filter(p => p.status === 'completed' && p.tlusty_success_method === 'seed_step').length;
|
||||
const synspec = pts.filter(p => psSlot(p) === 'synspec').length;
|
||||
const failed = pts.filter(p => p.status === 'failed').length;
|
||||
const phase = ctx.phaseView || 'tlusty';
|
||||
// 按当前阶段统计各档位计数(与 psSlot 同口径)
|
||||
const slotCount = {};
|
||||
pts.forEach(p => { const s = psSlot(p, phase); slotCount[s] = (slotCount[s] || 0) + 1; });
|
||||
const failed = slotCount.failed || 0;
|
||||
const baseFail = failed / total;
|
||||
|
||||
const findings = [];
|
||||
@@ -388,17 +426,18 @@ function renderPsConclusion(ctx, pts) {
|
||||
});
|
||||
[...groups.entries()].forEach(([v, g]) => {
|
||||
if (g.length < 3) return;
|
||||
const gf = g.filter(p => p.status === 'failed').length / g.length;
|
||||
const gf = g.filter(p => psSlot(p, phase) === 'failed').length / g.length;
|
||||
if (gf >= 0.3 && gf - baseFail >= 0.15) {
|
||||
findings.push({ score: gf - baseFail, text: `⚠ ${d}=${psFmtDim(d, v)}:失败率 ${Math.round(gf * 100)}%(${g.length} 点,均值 ${Math.round(baseFail * 100)}%)` });
|
||||
}
|
||||
});
|
||||
});
|
||||
findings.sort((a, b) => b.score - a.score);
|
||||
const lines = [
|
||||
`冷启动 ${cold} · 种子步进 ${seed} · SYNSPEC ${synspec} · 失败 ${failed} / 共 ${total}`,
|
||||
...findings.slice(0, 3).map(f => f.text),
|
||||
];
|
||||
// 结论行按阶段拼装:TLUSTY 视图拆 cold/seed/failed;SYNSPEC 视图拆 converged/failed/pending。
|
||||
const summary = phase === 'synspec'
|
||||
? `光谱有效 ${slotCount.converged || 0} · 光谱失败 ${slotCount.failed || 0} · 未运行 ${slotCount.pending || 0} / 共 ${total}`
|
||||
: `冷启动 ${slotCount.cold || 0} · 种子步进 ${slotCount.seed || 0} · 失败 ${slotCount.failed || 0} / 共 ${total}`;
|
||||
const lines = [summary, ...findings.slice(0, 3).map(f => f.text)];
|
||||
el.innerHTML = lines.map(l => `<span class="ps-concl-line">${escapeHtml(l)}</span>`).join('');
|
||||
|
||||
// 屏幕阅读器文本摘要(screen-reader-summary):平行集合图是 SVG role=img,
|
||||
@@ -460,8 +499,9 @@ function bindPsHover(ctx) {
|
||||
const dim = seg.getAttribute('data-dim');
|
||||
const val = seg.getAttribute('data-val');
|
||||
const slot = seg.getAttribute('data-slot');
|
||||
const phase = ctx.phaseView || 'tlusty';
|
||||
const names = ctx.psState.points
|
||||
.filter(p => psSlot(p) === slot && (dim === 'status' || Number(p[dim]) === Number(val)))
|
||||
.filter(p => psSlot(p, phase) === slot && (dim === 'status' || Number(p[dim]) === Number(val)))
|
||||
.map(p => p.name).slice(0, 200);
|
||||
if (names.length === 0) return;
|
||||
if (names.length === 1) { openPointPanel(ctx, names[0]); return; }
|
||||
@@ -476,8 +516,9 @@ function bindPsHover(ctx) {
|
||||
const dim = seg.getAttribute('data-dim');
|
||||
const val = seg.getAttribute('data-val');
|
||||
const slot = seg.getAttribute('data-slot');
|
||||
const phase = ctx.phaseView || 'tlusty';
|
||||
const names = ctx.psState.points
|
||||
.filter(p => psSlot(p) === slot && (dim === 'status' || Number(p[dim]) === Number(val)))
|
||||
.filter(p => psSlot(p, phase) === slot && (dim === 'status' || Number(p[dim]) === Number(val)))
|
||||
.map(p => p.name).slice(0, 200);
|
||||
if (names.length === 0) return;
|
||||
if (names.length === 1) { openPointPanel(ctx, names[0]); return; }
|
||||
|
||||
@@ -88,7 +88,7 @@ export async function openPointPanel(ctx, pointName) {
|
||||
const json = await res.json();
|
||||
if (!json.success || !json.data) throw new Error(json.message || '无数据');
|
||||
const { point, attempts, conv } = json.data;
|
||||
badges.innerHTML = `${pointStatusBadge(point.status)} ${pointMethodBadge(overallMethod(point))}`;
|
||||
badges.innerHTML = `${pointStatusBadge(point.status)} ${pointMethodBadge(overallMethod(point))} ${stageBadges(point)}`;
|
||||
body.innerHTML = '';
|
||||
body.appendChild(buildAttemptSection(attempts));
|
||||
body.appendChild(buildConvSection(conv));
|
||||
@@ -106,6 +106,21 @@ function attemptStageBadge(stage) {
|
||||
return `<span class="method-badge ${cls}">${label}</span>`;
|
||||
}
|
||||
|
||||
/** 阶段状态双徽标(TLUSTY ✓/✗ · SYNSPEC ✓/✗):从 DB 阶段列读 tlusty_status/synspec_status,
|
||||
* 让半失败点(大气收敛+光谱失败)一打开就能看到「整体失败 · TLUSTY ✓ · SYNSPEC ✗」。
|
||||
* 旧数据无阶段列(NULL)→ 不渲染(保持向后兼容)。 */
|
||||
function stageBadges(point) {
|
||||
const mk = (label, status) => {
|
||||
if (status === 'converged') return `<span class="stage-badge stage-ok">${label} ✓</span>`;
|
||||
if (status === 'failed') return `<span class="stage-badge stage-fail">${label} ✗</span>`;
|
||||
if (status === 'pending') return `<span class="stage-badge stage-pending">${label} …</span>`;
|
||||
return ''; // NULL → 不渲染
|
||||
};
|
||||
const t = mk('TLUSTY', point.tlusty_status);
|
||||
const s = mk('SYNSPEC', point.synspec_status);
|
||||
return (t || s) ? `${t} ${s}`.trim() : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 summary_json(ModelSummary)推导尝试方法徽标(Phase 6 起无 task_type 字段)。
|
||||
* ModelSummary.seed 存在 → 种子步进热启动;否则冷启动。synspec-only 任务无 seed →
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
renderStrip, renderOverview, renderRateLine, renderSparkline, maybeRefreshProgress,
|
||||
} from './detail/overview.js';
|
||||
import { renderPointsTab, refreshPoints, exportPointsCsv, updateSortIndicators } from './detail/pointsTable.js';
|
||||
import { renderAnalysisTab, refreshPsData, closePsPointList, reflowParSets } from './detail/parSets.js';
|
||||
import { renderAnalysisTab, refreshPsData, closePsPointList, reflowParSets, cleanupParSetsListeners } from './detail/parSets.js';
|
||||
import { openPointPanel, closePointPanel } from './detail/pointPanel.js';
|
||||
import { mountEnginePanel, unmountEnginePanel, refreshEnginePanel } from '../components/wfEnginePanel.js';
|
||||
|
||||
@@ -63,6 +63,7 @@ export function unmountWorkflowDetail() {
|
||||
ctx.psTooltipEl.remove();
|
||||
ctx.psTooltipEl = null;
|
||||
}
|
||||
cleanupParSetsListeners(ctx);
|
||||
ctx.abortCtl.abort();
|
||||
ctx = null;
|
||||
}
|
||||
@@ -122,7 +123,7 @@ async function refreshStats(name) {
|
||||
const json = await res.json();
|
||||
if (json.success && json.data) {
|
||||
ctx.latestStats = json.data;
|
||||
renderStrip(json.data);
|
||||
renderStrip(json.data, ctx.phaseView);
|
||||
if (ctx.activeTab === 'overview') {
|
||||
renderOverview(ctx, json.data);
|
||||
// 面板不再每 tick 重建、曲线 DOM 保留:只需把速率行/ETA/停滞横幅与最新统计同步
|
||||
@@ -156,7 +157,13 @@ function pageSkeleton(name) {
|
||||
</div>
|
||||
</div>
|
||||
<div id="wf-seg-progress" class="seg-progress" aria-hidden="true"></div>
|
||||
<div id="wf-strip-counts" class="wf-strip-counts tabular-num">正在获取执行数据…</div>
|
||||
<div class="wf-strip-row">
|
||||
<div class="phase-switcher" role="tablist" aria-label="阶段视图切换">
|
||||
<button type="button" class="phase-tab active" data-phase="tlusty" role="tab" aria-selected="true">TLUSTY(大气)</button>
|
||||
<button type="button" class="phase-tab" data-phase="synspec" role="tab" aria-selected="false">SYNSPEC(光谱)</button>
|
||||
</div>
|
||||
<div id="wf-strip-counts" class="wf-strip-counts tabular-num">正在获取执行数据…</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 内嵌执行引擎配置面板(启动/停止/删除/保存 + TLUSTY/SYNSPEC 阶段三维配置) -->
|
||||
|
||||
Reference in New Issue
Block a user