import { Activity, Ban, KeyRound, PackageCheck, Play, RefreshCw, RotateCcw, ShieldAlert, Square, Trash2, UploadCloud } from "lucide-react"; import { type ReactNode, useCallback, useEffect, useMemo, useState } from "react"; import { platformApiClient } from "../api/client"; import type { ClientManagerInstallationResponse, ClientManagerLifecycleOperation } from "../api/types"; import type { CurrentUserView } from "../contracts/workspace"; import type { OperationTracker } from "../stores/operations"; import { cx } from "../utils/classes"; import { ConfirmDialog } from "./OperationControls"; import { EmptyState, ErrorState, LoadingState, ResultBadge } from "./StateViews"; type LoadState = { status: "loading" } | { status: "error"; reason: string } | { status: "ready"; items: ClientManagerInstallationResponse[] }; interface ClientManagerLifecyclePanelProps { serverId: string; serverName: string; session: CurrentUserView; operations: OperationTracker; } interface PendingConfirmation { title: string; description: string; danger?: boolean; execute: () => Promise; } export function ClientManagerLifecyclePanel({ serverId, serverName, session, operations }: ClientManagerLifecyclePanelProps) { const [state, setState] = useState({ status: "loading" }); const [result, setResult] = useState<{ status: "pending" | "succeeded" | "failed"; label: string } | null>(null); const [confirmation, setConfirmation] = useState(null); const [confirmBusy, setConfirmBusy] = useState(false); const refresh = useCallback(async (showLoading = false) => { if (showLoading) setState({ status: "loading" }); try { const response = await platformApiClient.listClientManagerLifecycles(serverId); setState({ status: "ready", items: response.items }); } catch (error) { setState({ status: "error", reason: safeError(error, "Client Manager 状态加载失败") }); } }, [serverId]); useEffect(() => { void refresh(true); }, [refresh]); const hasActiveJob = state.status === "ready" && state.items.some((item) => item.job && ["queued", "accepted", "running", "retrying"].includes(item.job.state)); useEffect(() => { if (!hasActiveJob) return undefined; const timer = window.setInterval(() => void refresh(), 2500); return () => window.clearInterval(timer); }, [hasActiveJob, refresh]); async function runCommand(item: ClientManagerInstallationResponse, intent: string, execute: () => Promise) { const operationId = operations.begin({ intent, targetKind: "server", targetId: `${serverId}:client-manager:${item.profileKey}`, requester: session.displayName }); setResult({ status: "pending", label: `${intent} 已提交,等待 Platform/Run 返回真实状态` }); try { const next = await execute(); setState((current) => current.status === "ready" ? { status: "ready", items: current.items.map((entry) => entry.id === next.id ? next : entry) } : current); const label = next.job ? `${intent} 已排队,job ${next.job.id}` : `${intent} 已完成状态更新`; operations.succeed(operationId, label); setResult({ status: "succeeded", label }); await refresh(); } catch (error) { const reason = safeError(error, `${intent} 失败`); operations.fail(operationId, reason, operationId); setResult({ status: "failed", label: reason }); } } function confirmCommand(config: PendingConfirmation) { setConfirmation(config); } return (

Client Manager 生命周期

{result && }
{state.status === "loading" && } {state.status === "error" && void refresh(true)} />} {state.status === "ready" && state.items.length === 0 && } {state.status === "ready" && state.items.length > 0 && (
{state.items.map((item) => ( ))}
)} setConfirmation(null)} onConfirm={() => { if (!confirmation) return; setConfirmBusy(true); void confirmation.execute().finally(() => { setConfirmBusy(false); setConfirmation(null); }); }} />
); } interface ClientManagerLifecycleRowProps { item: ClientManagerInstallationResponse; serverName: string; runCommand: (item: ClientManagerInstallationResponse, intent: string, execute: () => Promise) => Promise; confirmCommand: (config: PendingConfirmation) => void; } function ClientManagerLifecycleRow({ item, serverName, runCommand, confirmCommand }: ClientManagerLifecycleRowProps) { const actionMap = useMemo(() => new Map(item.actions.map((action) => [action.operation, action])), [item.actions]); const available = (operation: ClientManagerLifecycleOperation) => actionMap.get(operation)?.available ?? false; const reason = (operation: ClientManagerLifecycleOperation) => actionMap.get(operation)?.reason ?? "Platform 当前状态不允许此操作"; const distributionId = item.distribution?.id ?? ""; const idempotency = (operation: string) => `client-manager.${operation}:${item.serverInstanceId}:${item.profileKey}:${Date.now()}`; const control = (operation: "start" | "stop" | "restart" | "status" | "rollback") => platformApiClient.controlClientManager(item.serverInstanceId, { profileKey: item.profileKey, operation, expectedDeploymentGeneration: item.deploymentGeneration, idempotencyKey: idempotency(operation) }); const deploy = () => runCommand(item, item.requiresRedeploy ? "重新部署 Client Manager" : "部署 Client Manager", () => platformApiClient.deployClientManager(item.serverInstanceId, { profileKey: item.profileKey, distributionId, expectedDeploymentGeneration: item.deploymentGeneration, idempotencyKey: idempotency("deploy") })); const update = () => runCommand(item, "更新 Client Manager", () => platformApiClient.updateClientManager(item.serverInstanceId, { profileKey: item.profileKey, distributionId, expectedDeploymentGeneration: item.deploymentGeneration, approved: true, idempotencyKey: idempotency("update") })); return (
{item.profileKey} {item.targetOs}/{item.targetArch} · deployment generation {item.deploymentGeneration} · key generation {item.keyGeneration}
{lifecycleLabel(item.status)} {healthLabel(item.health)}
组件身份{item.lastSeenAt ? `最后心跳 ${formatTime(item.lastSeenAt)}` : "等待独立注册"}{item.healthReason || "未收到安全健康原因"}
{item.phase || "等待生命周期事件"} {item.lastOperation && 最近操作 {item.lastOperation}} {item.lastSuccessfulJobId && 最近成功 job {item.lastSuccessfulJobId}}
{item.job && (
job {item.job.id} · attempt {item.job.attempt} · {item.job.state}{item.job.progress.percent}%
{item.job.progress.message || "等待 Run 回报真实阶段"}
)} {(item.retryable || item.requiresRedeploy || item.status === "failed") && (
{item.requiresRedeploy ? "组件密钥 generation 已变化:旧 artifact/session 已被围栏。请重新构建当前 generation,再执行重新部署。" : item.retryable ? "Run 保留了可恢复状态,可重试当前 intent;界面不会在 job 成功前推进阶段。" : "检查 Platform 审计与 job 失败原因后选择重新部署、回滚或卸载。"}
)}
} label={item.activeArtifactId ? "重新部署" : "部署"} disabled={!available("deploy") || !distributionId} reason={!distributionId ? "没有可用 distribution" : reason("deploy")} onClick={deploy} /> } label="启动" disabled={!available("start")} reason={reason("start")} onClick={() => void runCommand(item, "启动 Client Manager", () => control("start"))} /> } label="停止" disabled={!available("stop")} reason={reason("stop")} onClick={() => void runCommand(item, "停止 Client Manager", () => control("stop"))} /> } label="重启" disabled={!available("restart")} reason={reason("restart")} onClick={() => void runCommand(item, "重启 Client Manager", () => control("restart"))} /> } label="检查状态" disabled={!available("status")} reason={reason("status")} onClick={() => void runCommand(item, "检查 Client Manager 状态", () => control("status"))} /> } label="更新" disabled={!available("update") || !distributionId} reason={!distributionId ? "没有兼容的可用 distribution" : reason("update")} onClick={() => confirmCommand({ title: "批准 Client Manager 更新", description: `将 ${serverName} 的 ${item.profileKey} 从 ${item.activeVersion || "未安装"} 更新到 ${item.desiredVersion || "目标版本"}。Run 将 staged activate、健康确认,并在失败时恢复 previous slot。`, execute: update })} /> } label="回滚" disabled={!available("rollback")} reason={reason("rollback")} onClick={() => confirmCommand({ title: "回滚 Client Manager", description: `确认将 ${item.profileKey} 回滚到 ${item.previousVersion || "previous slot"}?当前组件 session 将被撤销并需要重新注册。`, danger: true, execute: () => runCommand(item, "回滚 Client Manager", () => control("rollback")) })} /> } label="重试" disabled={!item.retryable} reason="当前失败不可重试" onClick={() => void runCommand(item, "重试 Client Manager", () => platformApiClient.retryClientManagerLifecycle(item.serverInstanceId, { profileKey: item.profileKey, expectedDeploymentGeneration: item.deploymentGeneration, idempotencyKey: idempotency("retry") }))} /> } label="撤销会话" disabled={!item.activeArtifactId || item.status === "uninstalled"} reason="组件尚未安装" onClick={() => confirmCommand({ title: "撤销 Client Manager 会话", description: `撤销 ${item.profileKey} 的独立组件 session。Run session 与 job lease 不受影响,组件必须使用当前 key generation 重新注册。`, danger: true, execute: () => runCommand(item, "撤销 Client Manager 会话", () => platformApiClient.revokeClientManagerSession(item.serverInstanceId, { profileKey: item.profileKey, reason: "operator revoked component session" })) })} /> } label="重置密钥" disabled={item.status === "uninstalled"} reason="已卸载" onClick={() => confirmCommand({ title: "重置 Client Manager 密钥", description: `重置 ${item.profileKey} 的 component key 会撤销旧 session/artifact generation。必须重新构建并重新部署,不会显示或导出原始密钥。`, danger: true, execute: async () => { await platformApiClient.resetClientManagerKey(item.serverInstanceId, { componentKind: "client-manager", componentKey: item.profileKey }); await runCommand(item, "刷新密钥重置状态", () => platformApiClient.getClientManagerLifecycle(item.serverInstanceId, item.profileKey)); } })} /> } label="卸载" danger disabled={!available("uninstall")} reason={reason("uninstall")} onClick={() => confirmCommand({ title: "卸载 Client Manager", description: `确认停止并卸载 ${serverName} 的 ${item.profileKey}?Run 只会清理受控 Client Manager workspace,Platform 保留 build、artifact 与审计历史。`, danger: true, execute: () => runCommand(item, "卸载 Client Manager", () => platformApiClient.uninstallClientManager(item.serverInstanceId, { profileKey: item.profileKey, expectedDeploymentGeneration: item.deploymentGeneration, confirmed: true, idempotencyKey: idempotency("uninstall") })) })} />
); } function VersionCell({ label, version, revision, artifact }: { label: string; version?: string; revision?: string; artifact?: string }) { return
{label}{version || "--"}{revision ? `revision ${shortRef(revision)}` : "revision --"}{artifact ? ` · artifact ${shortRef(artifact)}` : ""}
; } function LifecycleButton({ icon, label, disabled, reason, danger, onClick }: { icon: ReactNode; label: string; disabled: boolean; reason: string; danger?: boolean; onClick: () => void }) { return ; } function lifecycleLabel(status: ClientManagerInstallationResponse["status"]): string { return ({ requested: "已请求", building: "构建中", available: "可部署", deploying: "部署中", installed: "已安装", registering: "等待注册", online: "在线", degraded: "降级", offline: "离线", updating: "更新中", rolling_back: "回滚中", stopping: "停止中", uninstalled: "已卸载", failed: "失败" })[status]; } function lifecycleTone(status: ClientManagerInstallationResponse["status"]): string { return ["online", "installed"].includes(status) ? "status-active" : ["failed", "offline", "uninstalled"].includes(status) ? "status-disabled" : "status-pending"; } function healthLabel(health: ClientManagerInstallationResponse["health"]): string { return ({ unknown: "健康未知", healthy: "健康", degraded: "健康降级", unhealthy: "不健康", offline: "心跳离线" })[health]; } function healthTone(health: ClientManagerInstallationResponse["health"]): string { return health === "healthy" ? "status-active" : health === "unknown" || health === "degraded" ? "status-pending" : "status-disabled"; } function shortRef(value: string): string { return value.length > 18 ? `${value.slice(0, 18)}…` : value; } function formatTime(value: string): string { const time = new Date(value); return Number.isNaN(time.getTime()) ? "未知" : time.toLocaleString(); } function safeError(error: unknown, fallback: string): string { const message = error instanceof Error ? error.message : fallback; return message.replace(/Bearer\s+\S+/gi, "[token]").replace(/sk-[A-Za-z0-9_-]+/g, "[secret]").slice(0, 240); }