feat(server,dashboard): 引入多工作流数据隔离、安全中间件与前端 ESM 模块化重构

- server: 实现按 workflow_name 的多工作流数据隔离与旧数据库平滑迁移机制
- server: 新增 API Key 认证(auth)、限流中间件(rate_limit)与运维备份接口(admin)
- server: 统一 AppError 错误处理体系,重构调度器 scheduler 支持工作流级重置与抢占
- node: 节点 ID 缺失时自动生成随机 UUID,原生支持 `docker compose --scale node=N` 动态扩容
- dashboard: 前端模块化重构(state/api/components),升级 CSS 变量设计系统与 Toast 通知
- docker/docs: 更新 /healthz 健康检查、部署脚本 IP 配置及数据库设计文档
This commit is contained in:
fmq
2026-07-28 21:54:02 +08:00
parent 4b4238d702
commit b91f1e4fa5
59 changed files with 7697 additions and 1490 deletions
+134
View File
@@ -0,0 +1,134 @@
/* DCTS Dashboard Application State & Polling Scheduler */
import { fetchClusterStatus, fetchWorkflowsList, fetchNodesList } from './api.js';
import { renderNodesTable } from './components/nodesTable.js';
import { renderWorkflows } from './components/workflows.js';
let isPolling = false;
let pollTimer = null;
export function isAppPolling() {
return isPolling;
}
export function startPolling() {
if (isPolling) return;
isPolling = true;
scheduleFetchData();
}
export function stopPolling() {
isPolling = false;
if (pollTimer) {
clearTimeout(pollTimer);
pollTimer = null;
}
}
export async function scheduleFetchData() {
if (pollTimer) clearTimeout(pollTimer);
await fetchAllData();
if (isPolling) {
pollTimer = setTimeout(scheduleFetchData, 5000);
}
}
export async function fetchAllData() {
await Promise.all([
fetchStatus(),
fetchWorkflows(),
fetchNodes(),
]);
}
export async function fetchStatus() {
try {
const data = await fetchClusterStatus();
updateUI(data);
updateServerStatus(true);
} catch (err) {
console.warn('获取 DCTS 状态失败:', err);
updateServerStatus(false);
}
}
export async function fetchWorkflows() {
try {
const json = await fetchWorkflowsList();
if (json.success && json.data) {
renderWorkflows(json.data);
}
} catch (err) {
console.warn('获取工作流列表失败:', err);
}
}
export async function fetchNodes() {
try {
const json = await fetchNodesList();
if (json.success && Array.isArray(json.data)) {
renderNodesTable(json.data);
return;
}
const data = await fetchClusterStatus();
if (data.nodes) {
renderNodesTable(data.nodes);
}
} catch (err) {
console.warn('获取计算节点列表失败:', err);
}
}
export function updateServerStatus(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');
if (valActiveNodes) valActiveNodes.textContent = activeNodesCount;
if (valTotalSlots) {
valTotalSlots.textContent = `${activeSlots} / ${maxSlots} CPU 槽位占用`;
}
const stats = data.grid_stats || {};
const pending = stats.pending ?? data.pending_points ?? 0;
const running = stats.running ?? data.running_points ?? 0;
const converged = stats.converged ?? data.converged_points ?? 0;
const failed = stats.failed ?? data.failed_points ?? 0;
const completed = converged + 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');
if (valCompletedTasks) valCompletedTasks.textContent = completed.toLocaleString();
if (valCompletionRate) {
valCompletionRate.textContent = `${converged.toLocaleString()} 收敛 / ${failed.toLocaleString()} 未收敛`;
}
const valTotalWorkflows = document.getElementById('val-total-workflows');
if (valTotalWorkflows && data.total_workflows !== undefined) {
valTotalWorkflows.textContent = data.total_workflows;
}
}