feat(all): 科学计算正确性修复、调度竞态消除、节点优雅退出、安全加固与收敛分析重构

科学计算正确性:
- 修复 Fortran 无-E 科学记数法(指数≥100 时 E 被挤掉,如 -1.35+118)导致
  发散行被静默跳过、误判收敛的 bug;扩展大气无效检测覆盖 Inf 与 *** 溢出标记
- 种子匹配改为 CNO 有向距离(富金属方向重罚 4×、贫金属方向轻罚 1×),
  基于 1191 个真实种子配对回测标定,回测净改善 314 个点
- GridAxisValue 反序列化拒绝非法文本(不再静默 NaN);chmax≤0 显式报错
- ions 行宽列宽对齐真实 fort.5 格式

调度与队列竞态:
- 原子选点(IMMEDIATE 事务 SELECT+UPDATE)消除并发调度重复派发 (#5)
- 调度互斥锁 + 冷启动优先策略(SeedStep 仅作失败后救援,不再正常路径热启动)
- 毒消息 dead_letter 标记防出队死循环;clear_queue 保留 claimed 行 (#6)
- 孤儿 running 点回收兜底;stale_sec 默认 7800→21600s(3 倍超时缓冲)

节点生命周期:
- SIGTERM+SIGINT 双信号监听(修复 Docker stop 发 SIGTERM 不触发优雅退出)
- SlotGuard RAII 防活动 slot 泄漏;子进程超时增加二级 30s wait 防 Fortran hang
- SeedStep 种子下载 fail-fast + 沙盒私有副本解耦 LRU 清理竞争
- reqwest Client 增加连接/请求超时;启动清理残留 task_* 沙盒

安全加固:
- 节点注册 registration_secret 二次凭据 (H8),恒定时间比对防时序旁路
- token 缓存 generation 机制消除 reissue 后旧 token TOCTOU 复活窗口
- 新增 /api/auth/logout 服务端 session 即时撤销;fail-closed 鉴权启动策略
- 前端 token 迁移 sessionStorage;YAML 高亮改 DOM API 消除 XSS 注入面
- 备份文件权限收紧 0600;点表动态值全面 escapeHtml

前端 Dashboard:
- 收敛性分析从热力图重构为 Parallel Sets 平行集合图(6 维+状态轴,手写 SVG 零依赖)
- 进度曲线横轴改为真实时间(服务端 now 锚定,停滞期诚实留白);轮询指数退避
- 移除 imported 收敛途径分类,导入点按实际途径 cold_run/seed_step 归类
- 初始化时 /api/auth/check 校验 token;401 toast 提示替代静默 reload

服务端恢复与工具链:
- 启动恢复 initializing 态工作流;默认工作流 INSERT-only 不覆盖 API 编辑
- body limit 分层(10MB 不再截断 256MB report);multipart 显式错误处理
- 嵌入二进制原子写(tmp+rename)防半写损坏
- import_results 判定收敛途径透传 success_method;conv.json 格式对齐本项目
- push_import_results.sh 退出码修复 + .bat UTF-8 BOM + scp 上传
- Docker USE_MIRRORS 默认关闭;移除无用 assets 挂载;删除 hosts.ini 入库
This commit is contained in:
fmq
2026-08-01 17:01:40 +08:00
parent 1bfa240cb0
commit c8fd24b120
45 changed files with 2821 additions and 831 deletions
+86 -15
View File
@@ -1,17 +1,35 @@
/* DCTS Dashboard API & Authentication Module */
import { showToast } from './components/toast.js';
// 安全:token 存储在 sessionStorage 而非 localStorage。
// sessionStorage 生命周期仅限当前标签页会话(关闭即失效),缩小了 XSS 窃取后的利用窗口;
// localStorage 会跨标签页/重启长期持久化,一旦泄露(配合 CSP 的 'unsafe-inline')即等于
// 长期账号接管。服务端 session 仍有 24h 过期 + 容量淘汰,前端不再持有永久凭据。
const TOKEN_KEY = 'dcts_admin_token';
export function getAdminToken() {
return localStorage.getItem(TOKEN_KEY);
try {
return sessionStorage.getItem(TOKEN_KEY);
} catch (_) {
return null;
}
}
export function setAdminToken(token) {
localStorage.setItem(TOKEN_KEY, token);
try {
sessionStorage.setItem(TOKEN_KEY, token);
} catch (_) {
/* sessionStorage 不可用时静默,鉴权将走 401 流程 */
}
}
export function clearAdminToken() {
localStorage.removeItem(TOKEN_KEY);
try {
sessionStorage.removeItem(TOKEN_KEY);
} catch (_) {
/* 同上 */
}
}
// 安全 HTML 转义,防止存储型 XSS
@@ -25,7 +43,14 @@ export function escapeHtml(s) {
.replace(/'/g, ''');
}
// 统一 fetch 包装:自动注入 Authorization 头,支持 10s 超时处理与 401 处理
// 401 处理去重:并发请求(如详情页 Promise.allSettled 三路拉取)可能各自命中 401
// 各触发一次 location.reload,竞态下不规范。用一个模块级 guard 保证只处理一次。
let handling401 = false;
/**
* 统一 fetch 包装:自动注入 Authorization 头,支持超时处理与 401 处理。
* 401 时清 token、提示会话过期并跳回登录,而非静默 reload(避免详情页突然闪回无说明)。
*/
export async function apiFetch(url, opts = {}, timeoutMs = 10000) {
const token = getAdminToken();
const headers = { ...(opts.headers || {}) };
@@ -35,30 +60,76 @@ export async function apiFetch(url, opts = {}, timeoutMs = 10000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
const signal = opts.signal || controller.signal;
// 调用方传入的 signal(如视图卸载 abortCtl)转发给内部 controller
// 否则 fetch 绑定的是调用方 signal,内部 timeoutMs 兜底会失效(请求挂起无法超时)。
const callerSignal = opts.signal;
const onCallerAbort = () => controller.abort();
if (callerSignal) {
if (callerSignal.aborted) {
controller.abort();
} else {
callerSignal.addEventListener('abort', onCallerAbort, { once: true });
}
}
// fetch 统一用内部 controller.signal:既能响应超时,也能响应调用方 abort。
const { signal: _ignored, ...optsWithoutSignal } = opts;
try {
const res = await fetch(url, { ...opts, headers, signal });
const res = await fetch(url, { ...optsWithoutSignal, headers, signal: controller.signal });
clearTimeout(timeoutId);
if (res.status === 401) {
if (res.status === 401 && !handling401) {
handling401 = true;
clearAdminToken();
location.reload();
// 延迟提示让 toast 可见,再回到登录视图
try {
showToast('会话已过期,请重新登录', 'error');
} catch (_) {
/* toast 不可用时跳过 */
}
setTimeout(() => {
location.hash = '';
location.reload();
}, 800);
}
return res;
} catch (err) {
clearTimeout(timeoutId);
throw err;
} finally {
// 清理调用方 signal 监听,避免泄漏(尤其长期存活的 abortCtl)。
if (callerSignal) callerSignal.removeEventListener('abort', onCallerAbort);
}
}
// API 请求接口定义
export async function loginAdmin(password) {
const res = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password }),
});
return res;
// loginAdmin 走统一超时包装,避免裸 fetch 在网络挂起时永不返回、按钮永久 disabled。
export async function loginAdmin(password, timeoutMs = 10000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const res = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password }),
signal: controller.signal,
});
return res;
} finally {
clearTimeout(timeoutId);
}
}
/**
* 登出:调用后端 /api/auth/logout 撤销当前 session(服务端立即失效),
* 再清除前端 token。即便 token 已被窃取,登出也能阻断重放(不再依赖 24h 自然过期)。
*/
export async function logoutAdmin() {
try {
await apiFetch('/api/auth/logout', { method: 'POST' }, 5000);
} catch (_) {
/* 网络异常时仍清前端 token,不阻断登出交互 */
}
clearAdminToken();
}
export async function fetchClusterStatus() {