+ {loadError && !status ?
: null}
+ {!status?.supported ?
: null}
+ {reconnecting ?
: null}
+ {status?.operation.phase === "manual_intervention" ?
: null}
+
+
+
}
+ title="可用版本"
+ description={status?.updateAvailable ? "发现比当前运行版本更新的发布构建。" : "检查 GitHub Release 与当前运行版本。"}
+ status={
}
+ footer={
+ <>
+
更新按钮不会跳过备份、镜像摘要、迁移或健康检查。
+
+ >
+ }
+ >
+
+
+
{status?.latestRelease?.version || "尚未检查"}
+ {status?.latestRelease?.prerelease ?
: null}
+
+
+ - 当前版本
- {status?.currentVersion || "未识别"}
+ - 部署方式
- {status?.deployment || "未知"}
+ - 发布时间
- {formatDate(status?.latestRelease?.publishedAt)}
+ - 代码仓库
- {status?.repository || "ddcat-ai/open-ai-canvas"}
+
+ {status?.latestRelease ? (
+
+
+
{status.latestRelease.body || "本版本未填写更新日志。"}
+
+ ) : null}
+
+
+
+
} title="更新前检查" description="所有阻断项通过后才允许切换服务。" status={{ label: status?.connected ? "更新器已连接" : "更新器未连接", color: status?.connected ? "success" : "error" }}>
+
+ {(status?.checks || []).map((check) => )}
+
+
+
+
+
} title="更新进度" description="操作状态和日志由宿主机持久化,服务重启后仍可读取。" status={
}>
+
+ {status?.operation.error ?
: null}
+ {(status?.operation.logs || []).length ? (
+
+ {status?.operation.logs.map((entry, index) => (
+ {phaseLabels[entry.phase]}{entry.message}
+ ))}
+
+ ) :
检查更新后,这里会显示完整操作记录。
}
+
+
+
+
}
+ title="备份与回退"
+ description="只使用最近一次通过 ZIP 结构与 SHA-256 校验的备份。"
+ status={{ label: status?.lastBackup ? "备份已校验" : "暂无备份", color: status?.lastBackup ? "success" : "warning" }}
+ footer={
+ <>
+
人工回退同样会执行停服、数据库恢复、旧镜像启动和健康验证。
+
} disabled={!status?.rollbackVersion || operationActive || !status?.connected} onClick={requestRollback}>回退到上一版本
+ >
+ }
+ >
+
+ {status?.lastBackup ?
- 备份编号
- {status.lastBackup.id}
- 来源版本
- {status.lastBackup.version}
- 创建时间
- {formatDate(status.lastBackup.createdAt)}
- 文件大小
- {formatBytes(status.lastBackup.size)}
- SHA-256
- {status.lastBackup.checksum}
:
首次开始更新时会自动创建数据库与数据目录 ZIP;备份失败将直接中止更新。
}
+
+
+
+
+ );
+}
+
+function PhaseBadge({ phase }: { phase: UpdatePhase }) {
+ const tone = phase === "succeeded" || phase === "ready" ? "success" : phase === "failed" || phase === "manual_intervention" ? "error" : phase === "rolling_back" || phase === "rolled_back" ? "warning" : activePhases.has(phase) ? "info" : "neutral";
+ return