Files
DCTS/dashboard/src/utils/format.js
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

158 lines
6.4 KiB
JavaScript
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.
/* DCTS 格式化与转义工具(权威定义)
*
* 集中所有纯函数:HTML 转义、时间/时长格式化、状态徽章。
* 历史上 escapeHtml 定义在 api.js、各 fmt* 散落在 workflowDetail.js,改一处要全局搜,
* 现统一到本模块。api.js 通过 re-export 保持向后兼容。
*
* SQLite 约定:datetime('now') 形如 'YYYY-MM-DD HH:MM:SS'UTC,无时区标记),
* 解析前需规范化为 ISO。所有时间解析函数共用 parseIso/parseTsMs。
*
* 语义与后端契约对齐(crates/server/src/db.rs):
* - 网格点状态:pending/queued/running/converged/failed/canceled
* - success_method(收敛途径):cold_run / seed_step
*/
/**
* 安全 HTML 转义,防止存储型 XSS。
* 顺序敏感:& 必须先转义,否则后续转义产生的 &amp; 会被二次转义。
* @param {unknown} s
* @returns {string}
*/
export function escapeHtml(s) {
if (s == null) return '';
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
/** 规范化为 Date(解析失败返回 null)。SQLite 'YYYY-MM-DD HH:MM:SS' 视为 UTC。 */
export function parseIso(iso) {
if (!iso) return null;
const normalized = iso.includes('T') ? iso : iso.replace(' ', 'T') + 'Z';
const d = new Date(normalized);
return Number.isNaN(d.getTime()) ? null : d;
}
/** 同 parseIso 但返回 epoch 毫秒(解析失败为 NaN)。供进度曲线按真实时间铺横轴。 */
export function parseTsMs(iso) {
if (!iso) return NaN;
const normalized = iso.includes('T') ? iso : iso.replace(' ', 'T') + 'Z';
return new Date(normalized).getTime();
}
/** SQLite datetime('now') 为 'YYYY-MM-DD HH:MM:SS'UTC 无时区标记),规范化后解析。 */
export function fmtTime(iso) {
if (!iso) return '—';
const normalized = iso.includes('T') ? iso : iso.replace(' ', 'T') + 'Z';
const d = new Date(normalized);
if (Number.isNaN(d.getTime())) return escapeHtml(iso);
return d.toLocaleTimeString('zh-CN');
}
/** 紧凑日期时间「MM-DD HH:MM」:带日期(跨天运行不至于看不出是哪天完成的),
* 隐去年份与秒(活跃网格数据不跨年,秒级精度放 title)。定宽 11 字符,列内对齐。 */
export function fmtDateTime(iso) {
const d = parseIso(iso);
if (!d) return iso ? escapeHtml(iso) : '—';
const p = (n) => String(n).padStart(2, '0');
return `${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
}
/** 完整时间戳「YYYY-MM-DD HH:MM:SS」,供单元格 title 悬停查看精确时刻。 */
export function fmtFullTs(iso) {
const d = parseIso(iso);
if (!d) return iso || '';
const p = (n) => String(n).padStart(2, '0');
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
}
/** 工作流级时长格式化:「N 分钟」或「N 小时 M 分」(控制条 ETA 用)。null/非有限值/≤0 返回占位符。 */
export function fmtDuration(sec) {
// ≤0 视为无数据(与 fmtDur 同口径):失败任务上报的 last_elapsed_sec 可能是 0.0
// runner 无 summary 时),显示"0 分钟"会误导,应展示占位符。
if (sec == null || !Number.isFinite(sec) || Number(sec) <= 0) return '—';
const m = Math.round(sec / 60);
if (m < 60) return `${m} 分钟`;
const h = Math.floor(m / 60);
return `${h} 小时 ${m % 60} 分`;
}
/** 网格点级紧凑时长(表格单元格用):42s / 12m 30s / 1h 20m。
* ≤0 视为无数据(tlusty 单点不可能 0 秒完成)。 */
export function fmtDur(sec) {
if (sec == null || sec === '') return '—';
const n = Number(sec);
if (!Number.isFinite(n) || n <= 0) return '—';
const s = Math.round(n);
if (s < 60) return `${s}s`;
const m = Math.floor(s / 60);
if (m < 60) return `${m}m${s % 60 ? ` ${s % 60}s` : ''}`;
const h = Math.floor(m / 60);
return `${h}h${m % 60 ? ` ${m % 60}m` : ''}`;
}
/** 相对收敛度指数记数(点表/尝试表 max_relc 单元格)。空值/非法值返回占位符。 */
export function fmtRelc(v) {
if (v == null || v === '') return '—';
const n = Number(v);
return Number.isFinite(n) ? n.toExponential(2) : '—';
}
/** CSV 字段转义:含逗号/引号/换行时用双引号包裹并翻倍内部引号。 */
export function csvEscape(v) {
const s = String(v ?? '');
return /[",\n\r]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
}
/** 心跳时间容错:字段缺失或非法时间戳一律显示占位符,避免渲染出 "Invalid Date"。 */
export function fmtClock(ts) {
if (!ts) return '—';
const d = new Date(ts);
if (Number.isNaN(d.getTime())) return '—';
return d.toLocaleTimeString('zh-CN');
}
// ===== 状态徽章(返回 HTML 字符串,供 innerHTML 注入) =====
/** 工作流状态映射:[中文标签, badge class]。后端可发 idle/initializing/running/paused/completed。 */
export const STATUS_MAP = {
running: ['运行中', 'online'],
completed: ['已完成', 'completed'],
initializing: ['初始化中', 'warning'],
paused: ['已暂停', 'secondary'],
idle: ['闲置', 'secondary'],
};
/** 工作流状态徽章 HTML。 */
export function statusBadge(status) {
const [cn, cls] = STATUS_MAP[status] || ['闲置', 'secondary'];
return `<span class="status-badge ${cls}">${cn}</span>`;
}
/** 网格点状态映射:[中文标签, badge class]。与后端 grid_points.status 对齐。 */
export const POINT_STATUS_MAP = {
converged: ['已收敛', 'online'],
failed: ['失败', 'danger'],
running: ['运行中', 'warning'],
queued: ['排队中', 'info'],
pending: ['未开始', 'secondary'],
};
/** 网格点状态徽章 HTML。 */
export function pointStatusBadge(status) {
const [cn, cls] = POINT_STATUS_MAP[status] || ['未知', 'secondary'];
return `<span class="status-badge ${cls}">${cn}</span>`;
}
/** 网格点收敛途径徽章 HTMLsuccess_methodcold_run / seed_step / 策略名)。 */
export function pointMethodBadge(method) {
if (method === 'cold_run') return '<span class="method-badge cold">冷启动</span>';
if (method === 'seed_step') return '<span class="method-badge seed">种子步进</span>';
// 其它策略名(如 synspec-only 任务归因的 "standard")原样展示,让光谱归因可见。
if (method) return `<span class="method-badge syn">${escapeHtml(method)}</span>`;
return '<span class="text-hint">—</span>';
}