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
+122
View File
@@ -0,0 +1,122 @@
/* DCTS Dashboard Nodes Table Component (增量 DOM 更新与 Skeleton) */
import { escapeHtml } from '../api.js';
export function renderNodesTableSkeleton() {
const tbody = document.getElementById('nodes-table-body');
if (!tbody) return;
const skeletonRows = Array.from({ length: 4 }).map(() => `
<tr class="skeleton-row">
<td><div class="skeleton-shimmer skeleton-text" style="width: 110px;"></div></td>
<td><div class="skeleton-shimmer skeleton-text" style="width: 90px;"></div></td>
<td><div class="skeleton-shimmer skeleton-text" style="width: 60px;"></div></td>
<td><div class="skeleton-shimmer skeleton-text" style="width: 80px;"></div></td>
<td><div class="skeleton-shimmer skeleton-badge"></div></td>
<td><div class="skeleton-shimmer skeleton-badge"></div></td>
<td><div class="skeleton-shimmer skeleton-text" style="width: 70px;"></div></td>
<td><div class="skeleton-shimmer skeleton-text" style="width: 60px;"></div></td>
</tr>
`).join('');
tbody.innerHTML = skeletonRows;
}
export function renderNodesTable(nodes) {
const tbody = document.getElementById('nodes-table-body');
const countBadge = document.getElementById('node-count-badge');
if (countBadge) {
countBadge.textContent = `${nodes ? nodes.length : 0} 个节点`;
}
if (!tbody) return;
if (!nodes || nodes.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="8" class="empty-cell">
暂无计算节点记录。启动 DCTS Node Worker 提交注册申请后,在此审批授权接入集群。
</td>
</tr>
`;
return;
}
// 构建新 HTML
const html = nodes.map(node => {
const nodeId = escapeHtml(node.node_id || node.id || 'N/A');
const hostName = escapeHtml(node.host_name || node.host || '127.0.0.1');
const isPending = node.status === 'pending_approval';
const isOnline = node.status === 'online' || node.status === 'active';
let statusBadge;
if (isPending) {
statusBadge = '<span class="status-badge warning"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" style="vertical-align: -1px; margin-right: 3px;"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>待审批</span>';
} else if (isOnline) {
statusBadge = '<span class="status-badge online">在线</span>';
} else {
statusBadge = '<span class="status-badge offline">离线</span>';
}
let tokenBadge;
if (isPending) {
tokenBadge = '<span class="status-badge secondary">待授权</span>';
} else if (node.token_status === 'active') {
tokenBadge = '<span class="status-badge online">有效</span>';
} else if (node.token_status === 'revoked') {
tokenBadge = '<span class="status-badge offline">已吊销</span>';
} else {
tokenBadge = '<span class="status-badge secondary">无凭据</span>';
}
const lastHeartbeat = node.last_heartbeat ? escapeHtml(new Date(node.last_heartbeat).toLocaleTimeString('zh-CN')) : '—';
const activeSlots = Number(node.active_slots || 0);
const maxSlots = Number(node.max_slots || 4);
const cpuUsage = Number(node.cpu_usage || 0).toFixed(1);
const memUsage = Number(node.memory_usage || 0).toFixed(1);
let actionBtns;
if (isPending) {
actionBtns = `
<div class="action-cell-wrap">
<button class="btn-action-icon btn-action-approve" data-node-action="approve" data-node-id="${nodeId}" title="同意节点接入申请" aria-label="同意节点接入申请">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="20 6 9 17 4 12"/></svg>
</button>
<button class="btn-action-icon btn-action-danger" data-node-action="reject" data-node-id="${nodeId}" title="拒绝节点接入申请" aria-label="拒绝节点接入申请">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
`;
} else {
actionBtns = `
<div class="action-cell-wrap">
<button class="btn-action-icon btn-action-reissue" data-node-action="reissue" data-node-id="${nodeId}" title="重新颁发专属 Token" aria-label="重新颁发专属 Token">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M23 4v6h-6M1 20v-6h6"/><path d="M3.51 9a9 9 0 0114.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0020.49 15"/></svg>
</button>
<button class="btn-action-icon btn-action-danger" data-node-action="revoke" data-node-id="${nodeId}" title="吊销 Token 凭据" aria-label="吊销 Token 凭据">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="12" cy="12" r="10"/><line x1="4.93" y1="4.93" x2="19.07" y2="19.07"/></svg>
</button>
</div>
`;
}
return `
<tr data-node-row-id="${nodeId}">
<td class="col-node-id node-id" title="${nodeId}">${nodeId}</td>
<td class="col-hostname" title="${hostName}">${hostName}</td>
<td class="col-slots tabular-num">${activeSlots} / ${maxSlots}</td>
<td class="col-usage tabular-num">${cpuUsage}% / ${memUsage}%</td>
<td class="col-status">${statusBadge}</td>
<td class="col-token">${tokenBadge}</td>
<td class="col-heartbeat tabular-num">${lastHeartbeat}</td>
<td class="col-actions">${actionBtns}</td>
</tr>
`;
}).join('');
// 增量 Diff 判断:如果内容一致,避重刷;否则进行更新
if (tbody.innerHTML.trim() !== html.trim()) {
tbody.innerHTML = html;
}
}