import { useQueryClient } from "@tanstack/react-query"; import type { HealthLevel, SystemHealth, SystemProcess, SystemWorkerLane } from "@umt/contracts"; import { AlertTriangle, ChevronDown, CircleCheck, Cpu, Database, HardDrive, ListTodo, RotateCw, Timer, XCircle, } from "lucide-react"; import { type ReactNode, useState } from "react"; import { Link } from "react-router"; import { useSystemHealth, useSystemHealthHistory } from "~/api/system.ts"; import { DataTable } from "~/components/console/data-table.tsx"; import { DetailSection } from "~/components/console/detail-sheet.tsx"; import { ErrorState, LoadingState } from "~/components/console/empty-state.tsx"; import { KvList } from "~/components/console/kv-list.tsx"; import { PageHeader } from "~/components/console/page-header.tsx"; import { StatusBadge, type StatusTone } from "~/components/console/status-badge.tsx"; import { TechInfo } from "~/components/console/tech-info.tsx"; import { AgentSpendCard, AttentionCard } from "~/components/status/attention-cards.tsx"; import { HistoryChart } from "~/components/status/history-chart.tsx"; import { ResourceSection } from "~/components/status/resource-section.tsx"; import { StorageSection } from "~/components/status/storage-section.tsx"; import { Button } from "~/components/ui/button.tsx"; import { Tabs, TabsList, TabsTrigger } from "~/components/ui/tabs.tsx"; import { formatDuration, formatRelative } from "~/lib/labels.ts"; import { qk } from "~/lib/query-keys.ts"; import { cn, formatBytes, formatDateTime } from "~/lib/utils.ts"; export const meta = () => [{ title: "运行状态 · UHD 媒体自动化控制台" }]; // 运行状态(W9-I):进程心跳、事件循环、SSE、DB、磁盘、挂载根、引擎与定时任务;存储节(FIX-5)见 storage-section.tsx。阈值见 docs/runbook/ops.md。 // 2026-10-06:顶部加「需要关注」汇总(系统健康逐条列出,铃铛点进来落到 #attention)与智能体用量。 // 界面第二轮:加「资源」节(CPU / 内存 / 网络 / 磁盘读写与趋势,参考 MP 仪表盘);进程表直接展示并带 CPU 列。 const LEVEL: Record = { ok: { label: "正常", tone: "success" }, warn: { label: "需要关注", tone: "warning" }, critical: { label: "需要处理", tone: "destructive" }, unknown: { label: "取不到数据", tone: "neutral" }, }; const KIND_LABEL: Record = { api: "API", worker: "Worker(后台任务)", "worker:agent": "Worker(智能体)", all: "单进程(all)", }; /** 横幅告警里的进程称呼 */ const KIND_NAME: Record = { api: "API", worker: "后台任务 worker", "worker:agent": "智能体 worker", all: "单进程", }; const MOUNT_REASON: Record = { root_missing: "目录不存在", sentinel_missing: "目录不可访问或未挂载", permission_denied: "没有权限", error: "检查出错", timeout: "检查超时", }; const HOURS = [1, 6, 24, 168]; function Card({ title, icon: Icon, level, children, action, }: { title: string; icon: typeof Cpu; level?: HealthLevel; children: ReactNode; action?: ReactNode; }) { return (

{title}

{level ? {LEVEL[level].label} : null} {action ? {action} : null}
{children}
); } /** 顶部横幅:整体档位 + 需要看的几条 */ function problems(d: SystemHealth): string[] { const out: string[] = []; // 只看当前实例;已下线实例不告警 for (const p of d.processes.filter((x) => x.stale)) { const line = `${KIND_NAME[p.processKind] ?? p.processKind} 45 秒没有心跳`; if (!out.includes(line)) out.push(line); } if (!d.processes.some((p) => !p.stale && p.processKind !== "api")) out.push("没有在线的 worker,任务不会被执行"); const lag = d.processes.filter((p) => !p.stale && p.eventLoopLagMaxMs >= 1000); if (lag.length) out.push(`${lag.length} 个进程事件循环延迟超过 1 秒`); if (d.db.level === "warn" || d.db.level === "critical") out.push("数据库延迟或连接数偏高"); const disks = d.disks.filter((x) => x.level === "warn" || x.level === "critical"); if (disks.length) out.push(`${disks.length} 个磁盘用量超过阈值`); const mounts = d.mounts.filter((m) => m.status !== "online"); if (mounts.length) out.push(`${mounts.length} 个挂载点不可用`); if (d.engine.level === "warn" || d.engine.level === "critical") out.push("任务积压或失败率偏高"); if (d.schedules.failing.length) out.push(`${d.schedules.failing.length} 个定时任务上次运行失败`); if (d.storage.level === "warn" || d.storage.level === "critical") out.push("存储用量超过告警线或预算,见「存储」"); return out; } function Banner({ d }: { d: SystemHealth }) { const lv = LEVEL[d.level]; const list = problems(d); const ok = d.level === "ok"; return (
{ok ? ( ) : d.level === "critical" ? ( ) : ( )}

{ok ? "全部正常" : lv.label} 检查于 {formatRelative(d.checkedAt)} · API {d.api.version},已运行 {formatDuration(d.api.uptimeSec)}

{list.length ? (
    {list.map((x) => (
  • {x}
  • ))}
) : null}
); } function Processes({ d }: { d: SystemHealth }) { return ( rows={d.processes} rowKey={(p) => p.instanceId} empty={{ title: "没有在线的进程", description: "进程启动后每 15 秒写一次心跳。" }} columns={[ { key: "kind", header: "进程", cell: (p) => (

{KIND_LABEL[p.processKind] ?? p.processKind} {p.stale ? 无心跳 : null}

{p.host} · pid {p.pid}

), }, { key: "ver", header: "版本", hideBelow: "lg", cell: (p) => {p.version}, }, { key: "start", header: "启动", hideBelow: "md", cell: (p) => ( {formatRelative(p.startedAt)} ), }, { key: "beat", header: "最后心跳", cell: (p) => ( {formatDuration(p.beatAgeSec)}前 ), }, { key: "lag", header: "延迟 / 峰值", align: "right", cell: (p) => ( = 5000 ? "text-destructive" : p.eventLoopLagMaxMs >= 1000 ? "text-warning" : "", )} > {p.eventLoopLagMs} / {p.eventLoopLagMaxMs} 毫秒 ), }, { key: "conc", header: "并发", hideBelow: "md", align: "right", cell: (p) => p.concurrency.io === undefined && p.concurrency.agent === undefined ? ( — ) : p.lanes.length ? ( ) : ( io {p.concurrency.io ?? 0} · 智能体 {p.concurrency.agent ?? 0} ), }, { key: "running", header: "运行中", hideBelow: "sm", align: "right", cell: (p) => {p.runningRuns ?? "—"}, }, { key: "sse", header: "SSE", hideBelow: "lg", align: "right", cell: (p) => {p.sseConnections ?? "—"}, }, { key: "cpu", header: "CPU", hideBelow: "md", align: "right", cell: (p) => { const cpu = p.extra.cpuPercent; return ( {typeof cpu === "number" ? `${cpu.toFixed(1)}%` : "—"} ); }, }, { key: "mem", header: "内存", hideBelow: "md", align: "right", cell: (p) => { const rss = p.extra.rssBytes; return {typeof rss === "number" ? formatBytes(rss, 0) : "—"}; }, }, ]} /> ); } const LANE_LABEL: Record = { file: "文件", network: "网络", maintenance: "维护" }; /** io worker 分道(WK-1):每道一行「运行中/并发 · 排队」,维护道让路时标出来 */ function LaneCell({ p }: { p: SystemProcess }) { return (
{p.lanes.map((l) => ( {LANE_LABEL[l.lane]} {l.running}/{l.concurrency} {l.queued ? ` · 排队 ${l.queued}` : ""} {l.yielding ? " · 让路中" : ""} ))} {p.concurrency.agent !== undefined ? ( 智能体 {p.concurrency.agent} ) : null}
); } /** 已下线实例:已退出、容器已重建或被同容器新进程替换的旧心跳行,默认折叠,不参与告警 */ function RetiredProcesses({ rows }: { rows: SystemProcess[] }) { const [open, setOpen] = useState(false); if (!rows.length) return null; return (
{open ? (
rows={rows} rowKey={(p) => p.instanceId} empty={{ title: "没有已下线实例" }} columns={[ { key: "id", header: "实例", cell: (p) => (

{KIND_LABEL[p.processKind] ?? p.processKind} {p.stoppedAt ? 已退出 : null}

{p.instanceId}

), }, { key: "ver", header: "版本", hideBelow: "md", cell: (p) => {p.version}, }, { key: "start", header: "启动", hideBelow: "md", cell: (p) => ( {formatRelative(p.startedAt)} ), }, { key: "beat", header: "最后心跳", cell: (p) => ( {formatRelative(p.lastBeatAt)} ), }, ]} />
) : null}
); } function History() { const [hours, setHours] = useState(6); const h = useSystemHealthHistory(hours); return (

趋势

{h.data ? ( 每点 {formatDuration(h.data.bucketSec)} ) : null} setHours(Number(v))} className="ml-auto"> {HOURS.map((n) => ( {n === 168 ? "7 天" : `${n} 小时`} ))}
{h.isPending ? ( ) : h.isError ? ( h.refetch()} /> ) : (
)}
); } function Disks({ d }: { d: SystemHealth }) { const t = d.diskThresholds; const scope = { data_dir: "数据目录", library_root: "媒体库目录", download_dir: "下载目录" } as const; return ( x.level))}>

阈值:{t.warnPercent}% 需要关注,{t.criticalPercent}% 需要处理(取自缓存设置)

    {d.disks.map((x) => { const pct = x.usedRatio === null ? null : x.usedRatio * 100; return (
  • {scope[x.scope]} {x.name} {pct === null ? (x.error ? `不可读 ${x.error}` : "用量未知") : `${pct.toFixed(0)}%`}

    {x.path} {x.totalBytes !== null && x.availableBytes !== null ? ` · 剩余 ${formatBytes(x.availableBytes, 1)} / ${formatBytes(x.totalBytes, 1)}` : ""}

  • ); })}
); } function worst(levels: HealthLevel[]): HealthLevel { if (levels.includes("critical")) return "critical"; if (levels.includes("warn")) return "warn"; if (levels.includes("ok")) return "ok"; return "unknown"; } function Mounts({ d }: { d: SystemHealth }) { const bad = d.mounts.filter((m) => m.status !== "online").length; return ( {d.mounts.length === 0 ? (

没有启用的媒体库目录、下载目录映射或 Alist 存储。

) : (
    {d.mounts.map((m) => (
  • {m.kind === "library_root" ? "媒体库目录" : m.kind === "storage_instance" ? "Alist 存储" : "下载目录"} {m.name}

    {m.path} {m.kind === "storage_instance" ? " · 连接与根路径" : m.sentinelChecked ? "" : " · 只查目录可读"}

    {m.status === "online" ? "在线" : m.status === "timeout" ? "超时" : "离线"} {m.reason && m.status !== "timeout" ? ` · ${ m.kind === "storage_instance" && m.reason === "permission_denied" ? "令牌失效或没有权限" : (MOUNT_REASON[m.reason] ?? m.reason) }` : ""}
  • ))}
)}
); } export default function StatusPage() { const qc = useQueryClient(); const h = useSystemHealth(); const d = h.data; return ( <> qc.invalidateQueries({ queryKey: qk.all("system") })} disabled={h.isFetching} > 刷新 } /> {h.isPending ? ( ) : h.isError ? ( h.refetch()} /> ) : d ? (
{d.db.error ?

{d.db.error}

: null}
查看任务 } > 查看定时任务 } > {d.schedules.failing.length ? (
    {d.schedules.failing.map((f) => (
  • {f.name} {formatRelative(f.lastRunAt)} {f.lastError ?

    {f.lastError}

    : null}
  • ))}
) : null}

进程 SSE 连接合计 {d.sse.connections}

) : null} ); }