Remove run node UI and expire registrations

This commit is contained in:
npc0-hue
2026-08-21 12:03:06 +08:00
parent b5a366e30d
commit fb5adf06d6
17 changed files with 175 additions and 206 deletions
+62 -131
View File
@@ -1,8 +1,8 @@
import { ListChecks, RotateCcw, ServerCog, Sparkles, WandSparkles } from "lucide-react";
import { AlertTriangle, ListChecks, RotateCcw, Sparkles, WandSparkles } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { platformApiClient } from "../api/client";
import type { JobResponse, RunEndpointResponse, ServerInstanceResponse } from "../api/types";
import type { JobResponse, ServerInstanceResponse } from "../api/types";
import { EmptyState, ErrorState, LoadingState, ResultBadge } from "../components/StateViews";
import { jobCapabilityLabel } from "../contracts/jobPresentation";
import type { PageComponentProps } from "../contracts/page";
@@ -11,21 +11,10 @@ import { cx } from "../utils/classes";
type ModuleState<T> = { status: "loading" } | { status: "error"; reason: string } | { status: "ready"; data: T };
export function MaintenancePage({ session, operations, onNavigate }: PageComponentProps) {
const [endpoints, setEndpoints] = useState<ModuleState<RunEndpointResponse[]>>({ status: "loading" });
const [jobs, setJobs] = useState<ModuleState<JobResponse[]>>({ status: "loading" });
const [servers, setServers] = useState<ModuleState<ServerInstanceResponse[]>>({ status: "loading" });
const [triageResult, setTriageResult] = useState<{ status: "pending" | "succeeded" | "failed"; label: string } | null>(null);
const refreshEndpoints = useCallback(async () => {
setEndpoints({ status: "loading" });
try {
const response = await platformApiClient.listRunEndpoints();
setEndpoints({ status: "ready", data: response.items });
} catch (error) {
setEndpoints({ status: "error", reason: error instanceof Error ? error.message : "加载失败" });
}
}, []);
const refreshJobs = useCallback(async () => {
setJobs({ status: "loading" });
try {
@@ -47,22 +36,19 @@ export function MaintenancePage({ session, operations, onNavigate }: PageCompone
}, []);
const refreshAll = useCallback(() => {
void refreshEndpoints();
void refreshJobs();
void refreshServers();
}, [refreshEndpoints, refreshJobs, refreshServers]);
}, [refreshJobs, refreshServers]);
useEffect(() => {
refreshAll();
}, [refreshAll]);
const endpointItems = endpoints.status === "ready" ? endpoints.data : [];
const jobItems = jobs.status === "ready" ? jobs.data : [];
const serverItems = servers.status === "ready" ? servers.data : [];
const failedJobs = useMemo(() => jobItems.filter((job) => job.state === "failed").slice(0, 8), [jobItems]);
const failedServers = useMemo(() => serverItems.filter((server) => server.state === "failed").slice(0, 8), [serverItems]);
const serverById = useMemo(() => new Map(serverItems.map((server) => [server.id, server])), [serverItems]);
const endpointById = useMemo(() => new Map(endpointItems.map((endpoint) => [endpoint.id, endpoint])), [endpointItems]);
const unhealthyEndpointCount = endpointItems.filter((endpoint) => endpoint.status !== "online" || heartbeatAgeMinutes(endpoint.lastHeartbeatAt) > 5).length;
async function retryJob(job: JobResponse) {
const retryStamp = Date.now();
@@ -106,15 +92,15 @@ export function MaintenancePage({ session, operations, onNavigate }: PageCompone
<div className="form-guidance maintenance-triage-intro">
<strong></strong>
<span> run </span>
<span></span>
</div>
<section className="maintenance-triage-grid" aria-label="维护排障入口">
<button type="button" className="triage-card" onClick={() => void refreshEndpoints()}>
<ServerCog size={18} />
<span></span>
<strong>{endpoints.status === "ready" ? `${unhealthyEndpointCount} 个需关注` : "加载中"}</strong>
<small></small>
<button type="button" className="triage-card" onClick={() => void refreshServers()}>
<AlertTriangle size={18} />
<span></span>
<strong>{servers.status === "ready" ? `${failedServers.length} 个需关注` : "加载中"}</strong>
<small></small>
</button>
<button type="button" className="triage-card" onClick={() => void refreshJobs()}>
<ListChecks size={18} />
@@ -125,64 +111,39 @@ export function MaintenancePage({ session, operations, onNavigate }: PageCompone
</section>
{triageResult && <ResultBadge status={triageResult.status} label={triageResult.label} />}
<section className="console-panel" aria-label="run endpoints">
<section className="console-panel" aria-label="failed servers">
<div className="panel-header">
<h2></h2>
<h2></h2>
</div>
{endpoints.status === "loading" && <LoadingState label="正在加载运行节点…" compact />}
{endpoints.status === "error" && (
<ErrorState title="运行节点加载失败" reason={endpoints.reason} diagnosticId="maintenance-endpoints" onRetry={() => void refreshEndpoints()} compact />
{servers.status === "loading" && <LoadingState label="正在加载服务器…" compact />}
{servers.status === "error" && <ErrorState title="服务器加载失败" reason={servers.reason} diagnosticId="maintenance-servers" onRetry={() => void refreshServers()} compact />}
{servers.status === "ready" && failedServers.length === 0 && (
<EmptyState title="暂无异常服务器" description="当前没有处于失败状态的服务器实例。" actionLabel="刷新服务器" onAction={() => void refreshServers()} />
)}
{servers.status === "error" && <ErrorState title="相关服务器加载失败" reason={servers.reason} diagnosticId="maintenance-servers" onRetry={() => void refreshServers()} compact />}
{endpoints.status === "ready" && endpoints.data.length === 0 && (
<EmptyState title="暂无运行节点" description="还没有运行端注册到平台。" actionLabel="刷新" onAction={() => void refreshEndpoints()} />
)}
{endpoints.status === "ready" && endpoints.data.length > 0 && (
<div className="resource-list maintenance-node-list">
{endpoints.data.map((endpoint) => {
const relatedServers = serverItems.filter((server) => server.runEndpointId === endpoint.id);
const firstRelatedServer = relatedServers[0];
return (
<article key={endpoint.id} className="resource-list-item maintenance-node-item">
<div>
<strong>{endpoint.displayName}</strong>
<span className="provider-id">{endpoint.id}</span>
</div>
<span className={cx("status-pill", endpointStatusClass(endpoint))}>{endpointStatusLabel(endpoint)}</span>
<span>
{endpoint.capacity.runningJobs}/{endpoint.capacity.maxJobs} {endpoint.capacity.queuedJobs}
</span>
<span>{heartbeatReason(endpoint)}</span>
<details className="node-detail">
<summary></summary>
<span> {endpoint.version}</span>
<span> {endpoint.capabilities.slice(0, 4).join(" / ") || "未上报"}</span>
<span> {relatedServers.length}</span>
</details>
<div className="console-row-actions">
<button type="button" className="theme-upload" onClick={() => void refreshEndpoints()}>
</button>
<button
type="button"
className="theme-upload"
disabled={!firstRelatedServer}
onClick={() => firstRelatedServer && onNavigate("serverDetail", { serverId: firstRelatedServer.id })}
>
</button>
<button
type="button"
className="theme-upload"
disabled={!firstRelatedServer}
onClick={() => firstRelatedServer && onNavigate("serverDetail", { serverId: firstRelatedServer.id })}
>
</button>
</div>
</article>
);
})}
{servers.status === "ready" && failedServers.length > 0 && (
<div className="console-record-list">
{failedServers.map((server) => (
<div key={server.id} className="console-record">
<div className="console-record-head">
<strong>{server.name}</strong>
<span className={cx("status-pill", "status-error")}>{serverStateLabel(server.state)}</span>
</div>
<div className="console-record-meta">
<span> <code>{server.id}</code></span>
<span> {server.pluginId}@{server.pluginVersion}</span>
<span>{formatTimestamp(server.updatedAt)}</span>
</div>
<div className="console-row-actions">
<button type="button" className="theme-upload" onClick={() => onNavigate("serverDetail", { serverId: server.id })}>
</button>
<button type="button" className="theme-upload" onClick={() => onNavigate("serverDetail", { serverId: server.id })}>
</button>
</div>
</div>
))}
</div>
)}
</section>
@@ -194,13 +155,12 @@ export function MaintenancePage({ session, operations, onNavigate }: PageCompone
{jobs.status === "loading" && <LoadingState label="正在加载任务…" compact />}
{jobs.status === "error" && <ErrorState title="任务加载失败" reason={jobs.reason} diagnosticId="maintenance-jobs" onRetry={() => void refreshJobs()} compact />}
{jobs.status === "ready" && failedJobs.length === 0 && (
<EmptyState title="暂无失败任务" description="最近任务没有失败记录;如果节点异常,请先查看运行节点心跳和容量。" actionLabel="刷新任务" onAction={() => void refreshJobs()} />
<EmptyState title="暂无失败任务" description="最近任务没有失败记录。" actionLabel="刷新任务" onAction={() => void refreshJobs()} />
)}
{jobs.status === "ready" && failedJobs.length > 0 && (
<div className="console-record-list">
{failedJobs.map((job) => {
const server = job.serverInstanceId ? serverById.get(job.serverInstanceId) : undefined;
const endpoint = endpointById.get(job.runEndpointId);
return (
<div key={job.id} className="console-record">
<div className="console-record-head">
@@ -208,15 +168,10 @@ export function MaintenancePage({ session, operations, onNavigate }: PageCompone
<span className="status-pill status-error">{jobStateLabel(job.state)}</span>
</div>
<div className="console-record-meta">
<span>
<code>{job.id}</code>
</span>
<span> <code>{job.id}</code></span>
<span> {server ? server.name : job.serverInstanceId ?? "平台任务"}</span>
<span> {endpoint ? endpoint.displayName : job.runEndpointId}</span>
<span>{progressMessage(job)}</span>
<span>
{job.attempt}/{job.retryPolicy.maxAttempts}
</span>
<span> {job.attempt}/{job.retryPolicy.maxAttempts}</span>
<span>{formatTimestamp(job.updatedAt)}</span>
</div>
<div className="console-row-actions">
@@ -237,53 +192,10 @@ export function MaintenancePage({ session, operations, onNavigate }: PageCompone
</div>
)}
</section>
</div>
);
}
function endpointStatusClass(endpoint: RunEndpointResponse): string {
if (endpoint.status === "online" && heartbeatAgeMinutes(endpoint.lastHeartbeatAt) <= 5) {
return "status-active";
}
if (endpoint.status === "offline") {
return "status-error";
}
return "status-disabled";
}
function endpointStatusLabel(endpoint: RunEndpointResponse): string {
if (endpoint.status === "online" && heartbeatAgeMinutes(endpoint.lastHeartbeatAt) > 5) {
return "心跳延迟";
}
return endpoint.status === "online" ? "在线" : endpoint.status === "offline" ? "离线" : endpoint.status;
}
function heartbeatReason(endpoint: RunEndpointResponse): string {
const age = heartbeatAgeMinutes(endpoint.lastHeartbeatAt);
if (!Number.isFinite(age)) {
return `心跳时间异常:${endpoint.lastHeartbeatAt}`;
}
if (endpoint.status === "offline") {
return `心跳异常:节点离线,最后 ${formatTimestamp(endpoint.lastHeartbeatAt)}`;
}
if (age > 5) {
return `心跳异常:${Math.round(age)} 分钟未更新`;
}
if (endpoint.capacity.runningJobs >= endpoint.capacity.maxJobs) {
return "容量已满:等待任务会继续排队";
}
return `心跳正常:${formatTimestamp(endpoint.lastHeartbeatAt)}`;
}
function heartbeatAgeMinutes(value: string): number {
const timestamp = new Date(value).getTime();
if (!Number.isFinite(timestamp)) {
return Number.POSITIVE_INFINITY;
}
return (Date.now() - timestamp) / 60000;
}
function jobStateLabel(state: JobResponse["state"]): string {
switch (state) {
case "queued":
@@ -303,6 +215,25 @@ function jobStateLabel(state: JobResponse["state"]): string {
}
}
function serverStateLabel(state: ServerInstanceResponse["state"]): string {
switch (state) {
case "running":
return "运行中";
case "stopped":
return "已停止";
case "installing":
return "安装中";
case "ready":
return "就绪";
case "deleted":
return "已删除";
case "draft":
return "草稿";
default:
return "失败";
}
}
function progressMessage(job: JobResponse): string {
return job.progress.message ? `原因 ${job.progress.message}` : `进度 ${job.progress.percent}%`;
}