- server/db: 拆 4929 行 db.rs 单体为 db/ 目录,migrations.rs 引入 PRAGMA user_version
版本化迁移运行器(M1~M13)
- 任务引擎 Phase 6/7b/7c 改名收敛:EngineStageConfig→PhaseConfig、StagePolicy→ResumePolicy、
Converged→Completed、删除 task_type 列、success_method 拆 tlusty_/synspec_ 双列、
新增 tlusty_status/synspec_status 半失败阶段守卫
- 科学正确性加固:conv_check 任意行 NaN/Inf/溢出判无效(0 行容忍)、新增 spec_is_valid
校验 SYNSPEC 脏谱、itek_history 逐次迭代全量保真、fmt_abn powf 溢出饱和
- 用户配置真正接通:tlusty_chain/tlusty_input 由死字段经 调度器→TaskSpec→executor→runner
透传生效;config 加载期 validate + deny_unknown_fields + 解析失败记 warn
- 调度修复:H1 活锁(pending_strategies 跳过已失败策略)、种子查找错误不再静默降级冷启动
- dashboard: 阶段配置面板 tlusty_stage/synspec_stage、"已完成"标签、迭代诊断展示
- docs: 新增 database_refactor_design.md,同步 database/api/PIPELINE/workflow_detail
147 lines
5.4 KiB
JavaScript
147 lines
5.4 KiB
JavaScript
/* DCTS Dashboard Application State & Polling Scheduler
|
||
*
|
||
* 首页全局轮询:5s 基准、指数退避上限 60s、Tab 切入后台自动暂停——
|
||
* 均由 utils/polling.js 的 createPoller 统一承担(与详情页作用域轮询共用一套实现)。
|
||
*/
|
||
|
||
import { fetchClusterStatus, fetchWorkflowsList, fetchNodesList } from './api.js';
|
||
import { renderNodesTable } from './components/nodesTable.js';
|
||
import { renderWorkflows } from './components/workflows.js';
|
||
import { createPoller } from './utils/polling.js';
|
||
import { logError } from './utils/errors.js';
|
||
|
||
let serverOnline = false;
|
||
|
||
// 全局轮询:fetchAllData 返回布尔(以 cluster 状态能否获取为整体健康判定)。
|
||
const poller = createPoller(fetchAllData, { baseMs: 5000, maxMs: 60000 });
|
||
|
||
export function isAppPolling() {
|
||
return poller.isRunning();
|
||
}
|
||
|
||
export function startPolling() {
|
||
poller.start();
|
||
}
|
||
|
||
export function stopPolling() {
|
||
poller.stop();
|
||
}
|
||
|
||
/** 手动立即触发一次全量拉取(兼容旧调用方:header 刷新 / 建工作流后 / wfActions 已改由
|
||
* isAppPolling 守卫,本函数保留给需要无条件刷新的调用点)。 */
|
||
export function scheduleFetchData() {
|
||
poller.triggerNow();
|
||
}
|
||
|
||
// 返回主状态拉取是否成功(用于轮询退避计数)。三项并行拉取,
|
||
// 以 cluster 状态能否成功获取作为整体健康判定。
|
||
export async function fetchAllData() {
|
||
const results = await Promise.allSettled([
|
||
fetchStatus(),
|
||
fetchWorkflows(),
|
||
fetchNodes(),
|
||
]);
|
||
// fetchStatus 内部已 try/catch,rejected 仅在极异常情况;以第一项(状态)成败为准。
|
||
return results[0].status === 'fulfilled' && serverOnline;
|
||
}
|
||
|
||
export async function fetchStatus() {
|
||
try {
|
||
const data = await fetchClusterStatus();
|
||
updateUI(data);
|
||
updateServerStatus(true);
|
||
} catch (err) {
|
||
logError(err, '获取 DCTS 状态失败');
|
||
updateServerStatus(false);
|
||
}
|
||
}
|
||
|
||
export async function fetchWorkflows() {
|
||
try {
|
||
const json = await fetchWorkflowsList();
|
||
if (json.success && json.data) {
|
||
renderWorkflows(json.data);
|
||
}
|
||
} catch (err) {
|
||
logError(err, '获取工作流列表失败');
|
||
}
|
||
}
|
||
|
||
export async function fetchNodes() {
|
||
try {
|
||
const json = await fetchNodesList();
|
||
if (json.success && Array.isArray(json.data)) {
|
||
renderNodesTable(json.data);
|
||
}
|
||
} catch (err) {
|
||
logError(err, '获取计算节点列表失败');
|
||
}
|
||
}
|
||
|
||
export function updateServerStatus(online) {
|
||
serverOnline = !!online;
|
||
const box = document.getElementById('status-indicator-box');
|
||
const text = document.getElementById('server-status-text');
|
||
if (!box || !text) return;
|
||
if (online) {
|
||
box.className = 'status-indicator online';
|
||
text.textContent = '服务端正常运行';
|
||
} else {
|
||
box.className = 'status-indicator offline';
|
||
text.textContent = '服务端连接中断';
|
||
}
|
||
}
|
||
|
||
export function updateUI(data) {
|
||
if (!data) return;
|
||
|
||
const activeNodesCount = data.nodes_online ?? data.active_nodes_count ?? (data.nodes ? data.nodes.length : 0);
|
||
const activeSlots = data.total_active_slots ?? data.occupied_slots ?? 0;
|
||
const maxSlots = data.total_max_slots ?? 0;
|
||
|
||
const valActiveNodes = document.getElementById('val-active-nodes');
|
||
const valTotalSlots = document.getElementById('val-total-slots');
|
||
const slotsProgressFill = document.getElementById('slots-progress-fill');
|
||
if (valActiveNodes) valActiveNodes.textContent = activeNodesCount;
|
||
if (valTotalSlots) {
|
||
valTotalSlots.textContent = `${activeSlots} / ${maxSlots} CPU 槽位占用`;
|
||
}
|
||
if (slotsProgressFill) {
|
||
const pct = maxSlots > 0 ? Math.min(100, Math.max(0, Math.round((activeSlots / maxSlots) * 100))) : 0;
|
||
slotsProgressFill.style.width = `${pct}%`;
|
||
}
|
||
|
||
const stats = data.grid_stats || {};
|
||
// 后端已将 queued 从 pending 拆出(详情页需区分"未入队/排队中");
|
||
// 首页"待计算网格点"保持传统合并口径 = pending + queued。
|
||
const pending = (stats.pending ?? 0) + (stats.queued ?? 0);
|
||
const running = stats.running ?? 0;
|
||
// 7c 改名:后端权威键为 stats.completed(原 converged)。doneOk=成功完成,failed=失败,
|
||
// doneTotal=两者合计(所有已终止点)。
|
||
const doneOk = stats.completed ?? 0;
|
||
const failed = stats.failed ?? 0;
|
||
const doneTotal = doneOk + failed;
|
||
|
||
const valPendingTasks = document.getElementById('val-pending-tasks');
|
||
const valRunningTasks = document.getElementById('val-running-tasks');
|
||
if (valPendingTasks) valPendingTasks.textContent = pending.toLocaleString();
|
||
if (valRunningTasks) {
|
||
valRunningTasks.textContent = `${running.toLocaleString()} 个任务计算中`;
|
||
}
|
||
|
||
const valCompletedTasks = document.getElementById('val-completed-tasks');
|
||
const valCompletionRate = document.getElementById('val-completion-rate');
|
||
const convergedProgressFill = document.getElementById('converged-progress-fill');
|
||
if (valCompletedTasks) valCompletedTasks.textContent = doneTotal.toLocaleString();
|
||
if (valCompletionRate) {
|
||
valCompletionRate.textContent = `${doneOk.toLocaleString()} 完成 / ${failed.toLocaleString()} 未完成`;
|
||
}
|
||
if (convergedProgressFill) {
|
||
const totalModels = pending + running + doneTotal;
|
||
const pct = totalModels > 0 ? Math.min(100, Math.max(0, Math.round((doneOk / totalModels) * 100))) : 0;
|
||
convergedProgressFill.style.width = `${pct}%`;
|
||
}
|
||
|
||
// 工作流总数由 renderWorkflows 从列表单独写入,避免与此处双源覆盖造成数字抖动
|
||
}
|