Files
DCTS/dashboard/__tab_test.mjs
T
fmq 1bfa240cb0 feat(all): 源精度命名体系、工作流可观测台、节点停用管理与白名单归档
核心变更:

  1. GridAxisValue 源精度命名
     - 新增 GridAxisValue 类型,携带 f64 数值 + YAML 源书写文本(Deref 透明兼容算术)
     - config.rs 绕过 serde_yaml 归一化,逐 token 捕获轴值原文(logg: 5.0 → g5.0)
     - runner/executor/scheduler 全链路改用 DB TEXT 列权威 point_name,
       修复 REAL 列回读丢精度导致的 model_name 错配

  2. 工作流执行可观测台
     - 新增 stats/progress/points 三组 API(进度时间序列、经验速率 ETA、
       停滞预警、逐点明细分页、收敛性热力图数据)
     - 新增 workflow_progress_snapshots 表 + tasks/grid_points 耗时列
     - runner 携带 last_iter/worst_depth/n_depths 进 conv.json
     - 前端新增 hash 路由、工作流详情页(概览/网格点/收敛分析三 Tab)、YAML 编辑器

  3. 节点停用/启用管理
     - 新增 disabled 状态 + disable/enable API;停用节点保持心跳但停止分发,
       worker 空闲待命而非退出;移除 revoke API,token 失效统一走重发覆盖;
       移除 host_name 字段

  4. 白名单结果归档
     - 新增 result_filter 模块,只归档有语义产物,丢弃 Tlusty 中间单元(~2MB/模型)
     - executor 原子写入归档 + 200 点 LRU 上限

  5. 历史数据导入
     - sync_seeds 重写为 import_results:经 /admin/import_seed 标记 converged +
       按新版命名迁移产物树

  6. 部署与目录重规划
     - data/results→seeds、data/archive→result + migrate_data_dirs.sh
     - deploy.sh 增强(SSH 复用、Profile、远程 env);Dockerfile 瘦身

  7. 文档同步更新 api/database/architecture/deployment
2026-07-31 01:34:05 +08:00

92 lines
4.5 KiB
JavaScript

import { JSDOM } from 'jsdom';
import fs from 'node:fs';
const html = fs.readFileSync('index.html', 'utf8');
const dom = new JSDOM(html, {
url: 'http://localhost:3000/#/workflows/sdB_cno',
runScripts: 'outside-only',
pretendToBeVisual: true,
});
const { window } = dom;
global.window = window;
global.document = window.document;
global.location = window.location;
global.localStorage = window.localStorage;
global.matchMedia =
window.matchMedia?.bind(window) ?? (() => ({ matches: false, addEventListener() {} }));
window.localStorage.setItem('dcts_admin_token', 'test-token');
const mkPoint = (teff, logc, status, method, attempts = 1, relc = 0.0005) => ({
name: `t${teff}_g5.0_he-2_c${logc}_n-4_o-4`,
teff, logg: 5.0, loghe: -2, logc, logn: -4, logo: -4,
cno_sum: logc - 8, wave: 0,
status, success_method: method, attempt_count: attempts,
last_max_relc: relc, last_task_type: method, seed_point_name: method === 'seed_step' ? 't20000_g5.0_he-2_c-4_n-4_o-4' : null,
node_id: 'node-a1', last_completed_at: status === 'converged' || status === 'failed' ? '2026-07-30 10:00:00' : null,
last_error: status === 'failed' ? 'nl stage diverged' : null,
});
const POINTS = [
mkPoint(20000, -4, 'converged', 'cold_run'),
mkPoint(40000, -4, 'converged', 'seed_step', 2),
mkPoint(60000, -4, 'failed', null, 2, 9.5e5),
mkPoint(20000, -3, 'running', null),
];
global.fetch = window.fetch = async (url) => {
const u = String(url);
if (u.includes('/stats')) {
return new Response(JSON.stringify({
success: true, message: 'ok',
data: { name: 'sdB_cno', status: 'running', total: 4, pending: 0, queued: 0, running: 1, converged: 2, failed: 1, cold_run_converged: 1, seed_step_converged: 1, imported_converged: 0, waves: [{ wave: 0, total: 4, converged: 2, failed: 1 }], avg_point_sec: 600, eta_sec: 600 },
}), { status: 200 });
}
if (u.includes('/points')) {
let pts = [...POINTS];
const q = new URL(u, 'http://localhost');
if (q.searchParams.get('status')) pts = pts.filter(p => p.status === q.searchParams.get('status'));
if (q.searchParams.get('method')) pts = pts.filter(p => p.success_method === q.searchParams.get('method'));
return new Response(JSON.stringify({ success: true, message: 'ok', data: { total: pts.length, points: pts } }), { status: 200 });
}
return new Response('{}', { status: 404 });
};
const { mountWorkflowDetail } = await import('./src/views/workflowDetail.js');
const tick = () => new Promise((r) => setTimeout(r, 30));
mountWorkflowDetail('sdB_cno');
await tick(); await tick();
// Tab2:点表
document.querySelector('[data-wf-tab="points"]').click();
await tick(); await tick();
console.log('points rows:', document.querySelectorAll('#points-table-body tr[data-point-row]').length === 4 ? 'OK' : 'MISSING');
console.log('rescued marker:', document.querySelector('#points-table-body')?.textContent.includes('↻') ? 'OK' : 'MISSING');
// 状态过滤 → 仅 failed
document.querySelector('[data-wf-filter="status"]').value = 'failed';
document.querySelector('[data-wf-filter="status"]').dispatchEvent(new window.Event('change', { bubbles: true }));
await tick();
console.log('status filter:', document.querySelectorAll('#points-table-body tr[data-point-row]').length === 1 ? 'OK' : 'WRONG');
// Tab3:热力图
document.querySelector('[data-wf-tab="analysis"]').click();
await tick(); await tick();
// logc 非轴且多值 → 切片选择器应出现并默认固定 logc=-4(3 点成格,c-3 点被切掉)
const sliceSel = document.querySelector('[data-heat-slice="logc"]');
console.log('slice selector:', sliceSel ? 'OK' : 'MISSING');
const cells = document.querySelectorAll('.heatmap-cell');
console.log('heatmap cells (sliced):', cells.length === 3 ? 'OK' : `GOT ${cells.length}`);
console.log('cold cell:', document.querySelector('.heatmap-cell.hc-cold') ? 'OK' : 'MISSING');
console.log('seed cell:', document.querySelector('.heatmap-cell.hc-seed') ? 'OK' : 'MISSING');
console.log('failed cell:', document.querySelector('.heatmap-cell.hc-failed') ? 'OK' : 'MISSING');
console.log('conclusion:', document.getElementById('heat-conclusion')?.textContent.includes('冷启动成功') ? 'OK' : 'MISSING');
// 切片切到 logc=-3 → 只剩 running 点一格
sliceSel.value = '-3';
sliceSel.dispatchEvent(new window.Event('change', { bubbles: true }));
await tick();
console.log('slice switch:', document.querySelectorAll('.heatmap-cell').length === 1 && document.querySelector('.heatmap-cell.hc-running') ? 'OK' : 'WRONG');
process.exit(0);