feat: add sync checkpoint restore for last successful sync
Capture rolling online DB snapshots after push/drain/reconcile and expose SyncPage 数据恢复 plus checkpoint/restore APIs; mark Z12h and restore done in coop docs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -34,6 +34,7 @@ import {
|
||||
createSyncChannel,
|
||||
deleteSyncChannel,
|
||||
dropSyncTable,
|
||||
getSyncCheckpointMeta,
|
||||
inspectSyncChannel,
|
||||
listAgents,
|
||||
listApps,
|
||||
@@ -44,6 +45,7 @@ import {
|
||||
revokeBindCode,
|
||||
previewSyncTable,
|
||||
reconcileSyncChannel,
|
||||
restoreSyncChannel,
|
||||
startSyncChannel,
|
||||
stopSyncChannel,
|
||||
testSyncEndpoints,
|
||||
@@ -308,6 +310,83 @@ export function SyncPage(props: {
|
||||
}
|
||||
}
|
||||
|
||||
function formatCheckpointTime(iso?: string) {
|
||||
if (!iso) return "—";
|
||||
const d = new Date(iso);
|
||||
if (Number.isNaN(d.getTime())) return iso;
|
||||
return d.toLocaleString();
|
||||
}
|
||||
|
||||
async function openRestore(r: SyncChannel) {
|
||||
setBusy(true);
|
||||
try {
|
||||
const meta = await getSyncCheckpointMeta(session, r.id);
|
||||
if (!meta.latest && !meta.previous) {
|
||||
message.warning("尚无成功同步快照。请先完成一次同步(含自动推送),约 30 秒后生成。");
|
||||
return;
|
||||
}
|
||||
let which: "latest" | "previous" = meta.latest ? "latest" : "previous";
|
||||
const options: { value: "latest" | "previous"; label: string }[] = [];
|
||||
if (meta.latest) {
|
||||
options.push({
|
||||
value: "latest",
|
||||
label: `最近一次 · ${formatCheckpointTime(meta.latest.synced_at)}(${meta.latest.row_count ?? 0} 行 / ${meta.latest.table_count ?? 0} 表)`,
|
||||
});
|
||||
}
|
||||
if (meta.previous) {
|
||||
options.push({
|
||||
value: "previous",
|
||||
label: `上一代 · ${formatCheckpointTime(meta.previous.synced_at)}(${meta.previous.row_count ?? 0} 行 / ${meta.previous.table_count ?? 0} 表)`,
|
||||
});
|
||||
}
|
||||
Modal.confirm({
|
||||
title: `数据恢复 · ${r.name || r.id}`,
|
||||
width: 560,
|
||||
content: (
|
||||
<div>
|
||||
<p style={{ marginTop: 0 }}>
|
||||
将<strong>线上库</strong>恢复为所选同步快照(upsert 并删除快照中不存在的行)。本机数据需宇恒下次同步/pull 从线上补回。
|
||||
</p>
|
||||
<p style={{ color: "rgba(0,0,0,0.45)", fontSize: 13 }}>
|
||||
若误删后又发生成功自动同步,最新快照可能已含删除后状态,请改选「上一代」。
|
||||
</p>
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<Typography.Text type="secondary">恢复到</Typography.Text>
|
||||
<Select
|
||||
style={{ width: "100%", marginTop: 6 }}
|
||||
defaultValue={which}
|
||||
options={options}
|
||||
onChange={(v) => {
|
||||
which = v;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
okText: "确认恢复",
|
||||
okType: "danger",
|
||||
cancelText: "取消",
|
||||
onOk: async () => {
|
||||
try {
|
||||
const res = await restoreSyncChannel(session, r.id, { which, confirm: true });
|
||||
message.success(
|
||||
`已恢复:写入 ${res.upserted ?? 0} 行,清理多余 ${res.deleted ?? 0} 行(快照 ${formatCheckpointTime(res.synced_at)})`
|
||||
);
|
||||
setInfo("线上库已按同步快照恢复");
|
||||
await refresh();
|
||||
} catch (e: any) {
|
||||
message.error(e.message || String(e));
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
});
|
||||
} catch (e: any) {
|
||||
message.error(e.message || String(e));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
function confirmDropTable(table: string) {
|
||||
if (!inspectCh) return;
|
||||
const sideLabel = inspectSide === "remote" ? "线上库" : "本机端(通道 local)";
|
||||
@@ -583,6 +662,9 @@ export function SyncPage(props: {
|
||||
>
|
||||
同步修复
|
||||
</Button>
|
||||
<Button size="small" onClick={() => void openRestore(r)}>
|
||||
数据恢复
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
danger
|
||||
|
||||
Reference in New Issue
Block a user