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:
+100
-16
@@ -1,13 +1,91 @@
|
||||
/* DCTS Dashboard API & Authentication Module */
|
||||
/* DCTS Dashboard API & Authentication Module
|
||||
*
|
||||
* 集中所有后端调用与鉴权逻辑。类型契约以 JSDoc 标注,供 IDE 提示与重构参考
|
||||
* (项目保持原生 ESM,不引入 TypeScript)。
|
||||
*
|
||||
* 安全:token 存储在 sessionStorage 而非 localStorage。
|
||||
* sessionStorage 生命周期仅限当前标签页会话(关闭即失效),缩小了 XSS 窃取后的利用窗口;
|
||||
* localStorage 会跨标签页/重启长期持久化,一旦泄露(配合 CSP 的 'unsafe-inline')即等于
|
||||
* 长期账号接管。服务端 session 仍有 24h 过期 + 容量淘汰,前端不再持有永久凭据。
|
||||
*/
|
||||
|
||||
import { showToast } from './components/toast.js';
|
||||
|
||||
// 安全:token 存储在 sessionStorage 而非 localStorage。
|
||||
// sessionStorage 生命周期仅限当前标签页会话(关闭即失效),缩小了 XSS 窃取后的利用窗口;
|
||||
// localStorage 会跨标签页/重启长期持久化,一旦泄露(配合 CSP 的 'unsafe-inline')即等于
|
||||
// 长期账号接管。服务端 session 仍有 24h 过期 + 容量淘汰,前端不再持有永久凭据。
|
||||
// escapeHtml 的权威定义已迁至 utils/format.js;此处 re-export 保持向后兼容
|
||||
// (nodesTable/workflows 等历史 import 自 api.js,改动成本高于收益)。
|
||||
export { escapeHtml } from './utils/format.js';
|
||||
|
||||
const TOKEN_KEY = 'dcts_admin_token';
|
||||
|
||||
/**
|
||||
* @typedef {Object} ApiResponse 通用响应包装
|
||||
* @property {boolean} success
|
||||
* @property {string} [message] 失败时的错误描述
|
||||
* @property {*} [data] 业务数据
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} ClusterStatus GET /api/status 返回的集群总体状态
|
||||
* @property {number} [nodes_online]
|
||||
* @property {number} [active_nodes_count]
|
||||
* @property {number} [total_active_slots]
|
||||
* @property {number} [occupied_slots]
|
||||
* @property {number} [total_max_slots]
|
||||
* @property {Array} [nodes]
|
||||
* @property {object} [grid_stats] - { pending, queued, running, converged, failed }
|
||||
* @property {number} [pending_points]
|
||||
* @property {number} [running_points]
|
||||
* @property {number} [converged_points]
|
||||
* @property {number} [failed_points]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} NodeInfo GET /api/admin/nodes 列表项
|
||||
* @property {string} node_id
|
||||
* @property {string} [id] - node_id 的别名(后端历史字段)
|
||||
* @property {string} status - online/active/disabled/pending_approval/offline
|
||||
* @property {string} token_status - active/inactive
|
||||
* @property {number} active_slots
|
||||
* @property {number} max_slots
|
||||
* @property {number} cpu_usage
|
||||
* @property {number} memory_usage
|
||||
* @property {string} last_heartbeat
|
||||
* @property {number|null} admin_max_slots - 管理员强制并发槽位上限(null=无限制)
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} Workflow GET /api/workflows 列表项
|
||||
* @property {string} name
|
||||
* @property {string} [description]
|
||||
* @property {string} status - idle/initializing/running/paused/completed
|
||||
* @property {string} [config_yaml]
|
||||
* @property {WorkflowStats} [stats] 列表接口内联的统计概要
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} WorkflowStats GET /api/workflows/:name/stats
|
||||
* @property {string} status
|
||||
* @property {number} total
|
||||
* @property {number} pending
|
||||
* @property {number} queued
|
||||
* @property {number} running
|
||||
* @property {number} converged
|
||||
* @property {number} failed
|
||||
* @property {number} [eta_sec]
|
||||
* @property {Array<{label:string,count:number}>} [waves]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} WorkflowPoint GET /api/workflows/:name/points 列表项
|
||||
* @property {string} name
|
||||
* @property {string} status
|
||||
* @property {string} [method]
|
||||
* @property {number} [relc]
|
||||
* @property {number} [runtime_sec]
|
||||
* @property {number} [attempts]
|
||||
* @property {string} [last_completed_at]
|
||||
*/
|
||||
|
||||
export function getAdminToken() {
|
||||
try {
|
||||
return sessionStorage.getItem(TOKEN_KEY);
|
||||
@@ -32,17 +110,6 @@ export function clearAdminToken() {
|
||||
}
|
||||
}
|
||||
|
||||
// 安全 HTML 转义,防止存储型 XSS
|
||||
export function escapeHtml(s) {
|
||||
if (s == null) return '';
|
||||
return String(s)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
// 401 处理去重:并发请求(如详情页 Promise.allSettled 三路拉取)可能各自命中 401 并
|
||||
// 各触发一次 location.reload,竞态下不规范。用一个模块级 guard 保证只处理一次。
|
||||
let handling401 = false;
|
||||
@@ -132,24 +199,28 @@ export async function logoutAdmin() {
|
||||
clearAdminToken();
|
||||
}
|
||||
|
||||
/** 获取集群总体状态(在线节点、槽位占用、网格点统计)。 */
|
||||
export async function fetchClusterStatus() {
|
||||
const res = await apiFetch('/api/status');
|
||||
if (!res.ok) throw new Error(`HTTP 错误代码: ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/** 获取工作流列表(含内联 stats 概要)。 */
|
||||
export async function fetchWorkflowsList() {
|
||||
const res = await apiFetch('/api/workflows');
|
||||
if (!res.ok) throw new Error(`HTTP 错误代码: ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/** 获取计算节点列表(管理员视角,含 token_status/cpu_usage 等)。 */
|
||||
export async function fetchNodesList() {
|
||||
const res = await apiFetch('/api/admin/nodes');
|
||||
if (!res.ok) throw new Error(`HTTP 错误代码: ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/** 创建工作流。 */
|
||||
export async function createWorkflow(workflowData) {
|
||||
return apiFetch('/api/workflows', {
|
||||
method: 'POST',
|
||||
@@ -235,3 +306,16 @@ export async function disableNodeApi(nodeId) {
|
||||
export async function enableNodeApi(nodeId) {
|
||||
return apiFetch(`/api/admin/nodes/${encodeURIComponent(nodeId)}/enable`, { method: 'POST' });
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置节点并发槽位配额(动态调整 CPU 核数)。
|
||||
* @param {string} nodeId
|
||||
* @param {number|null} adminMaxSlots - null=清除限制(恢复物理 max_slots);number=配额上限(0=暂停接新任务)
|
||||
*/
|
||||
export async function setNodeQuotaApi(nodeId, adminMaxSlots) {
|
||||
return apiFetch(`/api/admin/nodes/${encodeURIComponent(nodeId)}/quota`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ admin_max_slots: adminMaxSlots }),
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user