import { useConsole } from "../context"; import { workerAgeText } from "../api/client"; // Dashboard data cards: computed ONLY from data the store already // fetches (health, quota, tasks, workers, projects, envs, members, // skills/apps/MCP). No new API endpoint, no new API call. A card never // presents 0 as a real count when its dataset is not loaded or its // fetch failed: it shows 未加载 / 先选项目 / 加载失败 plus the // existing error string instead. function Card({ label, value, sub }: { label: string; value: string; sub: string }) { return (
{label}
{value}
{sub}
); } export default function DataCards() { const ctx = useConsole(); const healthValue = ctx.healthText === "未加载" ? "未加载" : ctx.healthText.includes("worker_ok=true") ? "正常" : ctx.healthText.includes("worker_ok=false") ? "异常" : ctx.healthText.includes("加载失败") ? "加载失败" : "未知"; const quotaMatch = ctx.quotaText.match(/剩余 \$([0-9.]+)/); const quotaValue = ctx.quotaText.includes("加载失败") ? "加载失败" : ctx.quotaText.includes("未填 key") || ctx.quotaText === "未加载" ? "未加载" : quotaMatch ? "$" + quotaMatch[1] : "配额"; // Status counts come from the server (all project rows), not from // ctx.tasks, which is now only the current page — counting the page // would present 20 tasks as the whole project. const statusCounts = (["running", "completed", "failed", "queued", "merged", "interrupted"] as const) .map((s) => s + " " + (ctx.taskStatusCounts[s] || 0)) .join(" | "); const selectedProject = ctx.projects.find((p) => p.id === ctx.projectId); const tasksCard = ctx.taskError ? { value: "加载失败", sub: ctx.taskError } : !ctx.projectId ? { value: "先选项目", sub: ctx.projectMsg || "项目加载后自动选第一个" } : { value: String(ctx.taskTotal), sub: ctx.taskCountText + " | " + statusCounts + (selectedProject ? " | " + selectedProject.name : "") }; let workersCard: { value: string; sub: string }; if (ctx.workersError) { workersCard = { value: "加载失败", sub: ctx.workersError }; } else if (ctx.workers === null) { workersCard = { value: ctx.workersSummary || "未加载", sub: "完整 worker 清单仅全局管理员可见" }; } else { const online = ctx.workers.filter((w) => { if (w.alive !== undefined && w.alive !== null) return !!w.alive; const age = workerAgeText(w).age; return age !== null && age <= 120; }).length; workersCard = { value: online + " 在线 / " + ctx.workers.length, sub: (ctx.workers.length - online) + " 离线" }; } const projectsCard = ctx.projectMsg.includes("加载失败") ? { value: "加载失败", sub: ctx.projectMsg } : { value: ctx.projects.length + " 个", sub: selectedProject ? "当前项目:" + selectedProject.name : "未选项目(加载后自动选第一个)" }; const projectErrors = [ctx.envMsg, ctx.memberError, ctx.skillError].filter((s) => s && s.includes("失败")); const projectDataCard = !ctx.projectId ? { value: "先选项目", sub: "选项目后显示环境 / 成员 / 技能计数" } : projectErrors.length ? { value: "部分加载失败", sub: projectErrors.join(" | ") } : { value: ctx.envs.length + " 个环境", sub: ctx.members.length + " 成员 | " + ctx.skills.length + " 技能 | " + ctx.apps.length + " 应用 | " + ctx.mcpServers.length + " MCP" }; return (
); }