dsh-config-manager 0.1.19 → 0.1.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/lib/client.d.ts +51 -0
  2. package/lib/client.js +284 -89
  3. package/lib/client.js.map +1 -1
  4. package/lib/index.js +135 -2
  5. package/lib/index.js.map +1 -1
  6. package/lib/sync/ancestor.d.ts +17 -0
  7. package/lib/sync/ancestor.js +64 -0
  8. package/lib/sync/ancestor.js.map +1 -0
  9. package/lib/sync/merge.d.ts +42 -0
  10. package/lib/sync/merge.js +325 -0
  11. package/lib/sync/merge.js.map +1 -0
  12. package/lib/sync/review-queue.d.ts +51 -0
  13. package/lib/sync/review-queue.js +119 -0
  14. package/lib/sync/review-queue.js.map +1 -0
  15. package/lib/sync/risk.d.ts +59 -0
  16. package/lib/sync/risk.js +76 -0
  17. package/lib/sync/risk.js.map +1 -0
  18. package/lib/sync/sync-config.d.ts +7 -2
  19. package/lib/sync/sync-config.js +24 -4
  20. package/lib/sync/sync-config.js.map +1 -1
  21. package/lib/sync/sync-engine.d.ts +32 -1
  22. package/lib/sync/sync-engine.js +207 -8
  23. package/lib/sync/sync-engine.js.map +1 -1
  24. package/lib/sync/sync-state.d.ts +7 -2
  25. package/lib/sync/sync-state.js +9 -5
  26. package/lib/sync/sync-state.js.map +1 -1
  27. package/package.json +1 -1
  28. package/src/client/sync/SyncHistoryView.test.ts +40 -0
  29. package/src/client/sync/SyncHistoryView.tsx +73 -0
  30. package/src/client/sync/SyncPullPreviewView.test.ts +54 -0
  31. package/src/client/sync/SyncPullPreviewView.tsx +165 -0
  32. package/src/client/sync/SyncSettingsView.tsx +88 -2
  33. package/src/client/sync/history-model.ts +25 -0
  34. package/src/client/sync/pull-preview-model.ts +44 -0
  35. package/src/client/sync/sync-api.ts +28 -1
  36. package/src/client/sync/sync-view.ts +1 -1
  37. package/src/index.ts +127 -2
  38. package/src/sync/ancestor.test.ts +152 -0
  39. package/src/sync/ancestor.ts +81 -0
  40. package/src/sync/merge.test.ts +204 -0
  41. package/src/sync/merge.ts +360 -0
  42. package/src/sync/review-queue.test.ts +120 -0
  43. package/src/sync/review-queue.ts +167 -0
  44. package/src/sync/risk.test.ts +131 -0
  45. package/src/sync/risk.ts +122 -0
  46. package/src/sync/sync-config.test.ts +106 -0
  47. package/src/sync/sync-config.ts +23 -4
  48. package/src/sync/sync-engine.test.ts +292 -3
  49. package/src/sync/sync-engine.ts +219 -8
  50. package/src/sync/sync-state.test.ts +48 -2
  51. package/src/sync/sync-state.ts +11 -5
package/lib/client.d.ts CHANGED
@@ -528,6 +528,25 @@ declare class ConfigManagerApi {
528
528
  restoreSnapshot(snapshotId: string, dryRun: boolean): Promise<RestoreResponse>;
529
529
  }
530
530
  //#endregion
531
+ //#region src/sync/review-queue.d.ts
532
+ /** 单条待审项(决策前 description/local/remote/ancestor 必填;decision 可选) */
533
+ interface ReviewQueueItem {
534
+ id: string;
535
+ /** 分区 id(settings / providers / workspaces / plugins / mcp 等) */
536
+ sectionId: string;
537
+ /** 'key' | 'file' | 'section':冲突粒度(来自 MergeConflict.kind 或整分区冲突) */
538
+ kind: 'key' | 'file' | 'section';
539
+ description: string;
540
+ local?: unknown;
541
+ remote?: unknown;
542
+ ancestor?: unknown;
543
+ /** 用户决策(resolve 后写入) */
544
+ decision?: 'useRemote' | 'keepLocal' | 'skip';
545
+ decidedAt?: string;
546
+ /** 关联的远端快照 id(追溯来源) */
547
+ snapshotId?: string;
548
+ }
549
+ //#endregion
531
550
  //#region src/sync/sync-engine.d.ts
532
551
  interface SyncPushReport {
533
552
  ok: boolean;
@@ -554,6 +573,20 @@ interface SyncPullReport {
554
573
  needsReview: boolean;
555
574
  message?: string;
556
575
  }
576
+ /** 自动应用执行器(P2b)报告 */
577
+ interface ApplyReport {
578
+ ok: boolean;
579
+ /** 实际写入本地的分区 id 列表 */
580
+ applied: string[];
581
+ /** 应用前快照 id(UI 可借此一键回滚);失败时仍透传以便排查 */
582
+ restoreId: string;
583
+ /** 是否触发了整体回滚 */
584
+ rolledBack: boolean;
585
+ /** 失败时移到 review 队列的项(ReviewQueueItem 形态供 UI 直接渲染) */
586
+ review: ReviewQueueItem[];
587
+ /** 来自 Importer.ImportResult 的 warnings;UI 可用于红条提示 */
588
+ warnings: string[];
589
+ }
557
590
  //#endregion
558
591
  //#region src/client/sync/sync-api.d.ts
559
592
  /** GET /sync/status 响应:配置/凭据/上次同步的只读事实(无任何 secret 值) */
@@ -632,6 +665,24 @@ declare class SyncApi {
632
665
  githubCancel(flowId: string): Promise<{
633
666
  ok: boolean;
634
667
  }>;
668
+ /** 同步历史:列出 localSnapshotsDir 下的快照目录(manifest.json: id/createdAt/sectionHashes) */
669
+ history(): Promise<SyncHistoryEntry[]>;
670
+ /** 应用自动应用计划:写本地 + backup + (失败时) rollback + enqueue review;返回 ApplyReport */
671
+ apply(payload: SyncPushPayload): Promise<ApplyReport>;
672
+ /** 一键回滚:按 restoreId 调用 backup→rollback */
673
+ rollback(payload: {
674
+ restoreId: string;
675
+ }): Promise<{
676
+ ok: boolean;
677
+ full: boolean;
678
+ }>;
679
+ }
680
+ /** 同步历史条目(Host 端返回) */
681
+ interface SyncHistoryEntry {
682
+ id: string;
683
+ createdAt: string;
684
+ sectionCount: number;
685
+ reviewCount: number;
635
686
  }
636
687
  //#endregion
637
688
  //#region src/client/client-types.d.ts
package/lib/client.js CHANGED
@@ -1949,104 +1949,104 @@ if (typeof document !== "undefined" && document.querySelector("style[data-tag=\"
1949
1949
  document.head.appendChild(tag);
1950
1950
  }
1951
1951
  var config_manager_module_css_default = {
1952
- "errorBanner": "_367UGW_errorBanner",
1953
- "snapshotRow": "_367UGW_snapshotRow",
1954
- "pathNew": "_367UGW_pathNew",
1955
- "hiddenFile": "_367UGW_hiddenFile",
1956
- "kindTag": "_367UGW_kindTag",
1957
- "field": "_367UGW_field",
1958
- "groupLabel": "_367UGW_groupLabel",
1959
- "groupHeader": "_367UGW_groupHeader",
1960
- "badgeWarn": "_367UGW_badgeWarn",
1961
- "errorReason": "_367UGW_errorReason",
1962
- "formError": "_367UGW_formError",
1963
- "pathOld": "_367UGW_pathOld",
1964
- "progressBlock": "_367UGW_progressBlock",
1965
- "card": "_367UGW_card",
1952
+ "warnText": "_367UGW_warnText",
1966
1953
  "checkboxRow": "_367UGW_checkboxRow",
1967
- "viewBody": "_367UGW_viewBody",
1968
- "sectionSubtitle": "_367UGW_sectionSubtitle",
1969
- "reportText": "_367UGW_reportText",
1970
- "progressPercent": "_367UGW_progressPercent",
1971
- "categoryName": "_367UGW_categoryName",
1972
- "progressBadge": "_367UGW_progressBadge",
1954
+ "progressDetail": "_367UGW_progressDetail",
1955
+ "conflictDetail": "_367UGW_conflictDetail",
1956
+ "kindTag": "_367UGW_kindTag",
1957
+ "modeTab": "_367UGW_modeTab",
1958
+ "groupList": "_367UGW_groupList",
1973
1959
  "progressBadgeSection": "_367UGW_progressBadgeSection",
1974
- "warnList": "_367UGW_warnList",
1975
- "actionRow": "_367UGW_actionRow",
1976
- "severityError": "_367UGW_severityError",
1977
- "progressLabel": "_367UGW_progressLabel",
1978
- "section": "_367UGW_section",
1979
- "badge": "_367UGW_badge",
1980
- "viewTab": "_367UGW_viewTab",
1981
- "conflictId": "_367UGW_conflictId",
1982
- "snapshotList": "_367UGW_snapshotList",
1983
- "reportList": "_367UGW_reportList",
1984
- "rowActions": "_367UGW_rowActions",
1985
- "viewTabs": "_367UGW_viewTabs",
1986
- "badgeInfo": "_367UGW_badgeInfo",
1987
- "groupNote": "_367UGW_groupNote",
1988
- "conflictList": "_367UGW_conflictList",
1989
- "categoryDesc": "_367UGW_categoryDesc",
1990
- "conflictItem": "_367UGW_conflictItem",
1991
- "sectionTitle": "_367UGW_sectionTitle",
1992
- "empty": "_367UGW_empty",
1960
+ "groupLabel": "_367UGW_groupLabel",
1993
1961
  "fieldLabel": "_367UGW_fieldLabel",
1994
- "progressTrack": "_367UGW_progressTrack",
1962
+ "groupCard": "_367UGW_groupCard",
1963
+ "hiddenFile": "_367UGW_hiddenFile",
1964
+ "conflictId": "_367UGW_conflictId",
1965
+ "spinner": "_367UGW_spinner",
1966
+ "section": "_367UGW_section",
1967
+ "progressMeta": "_367UGW_progressMeta",
1995
1968
  "reportScroll": "_367UGW_reportScroll",
1996
- "pathRow": "_367UGW_pathRow",
1997
- "modeTab": "_367UGW_modeTab",
1998
- "hint": "_367UGW_hint",
1969
+ "dshCmIndeterminate": "_367UGW_dshCmIndeterminate",
1970
+ "badgeOk": "_367UGW_badgeOk",
1971
+ "banner": "_367UGW_banner",
1972
+ "warnList": "_367UGW_warnList",
1973
+ "progressBadge": "_367UGW_progressBadge",
1974
+ "spinnerLabel": "_367UGW_spinnerLabel",
1975
+ "primaryButton": "_367UGW_primaryButton",
1976
+ "progressBarDone": "_367UGW_progressBarDone",
1999
1977
  "secretFields": "_367UGW_secretFields",
2000
- "reportView": "_367UGW_reportView",
2001
- "errorFooter": "_367UGW_errorFooter",
2002
- "pathMappingList": "_367UGW_pathMappingList",
2003
- "pathValue": "_367UGW_pathValue",
2004
- "spinner": "_367UGW_spinner",
2005
- "input": "_367UGW_input",
2006
- "progressIndeterminate": "_367UGW_progressIndeterminate",
2007
- "groupList": "_367UGW_groupList",
2008
- "progressDetail": "_367UGW_progressDetail",
2009
- "optionsCard": "_367UGW_optionsCard",
2010
- "ghostButton": "_367UGW_ghostButton",
2011
- "errorLine": "_367UGW_errorLine",
2012
- "modeTabs": "_367UGW_modeTabs",
2013
1978
  "progressBadgeCount": "_367UGW_progressBadgeCount",
2014
- "progressMeta": "_367UGW_progressMeta",
2015
- "errorTitle": "_367UGW_errorTitle",
1979
+ "pathValue": "_367UGW_pathValue",
2016
1980
  "groupItems": "_367UGW_groupItems",
2017
- "sectionHeader": "_367UGW_sectionHeader",
2018
1981
  "errorAction": "_367UGW_errorAction",
2019
- "errorActionLabel": "_367UGW_errorActionLabel",
2020
- "dangerButton": "_367UGW_dangerButton",
2021
- "errorItem": "_367UGW_errorItem",
2022
- "secretsList": "_367UGW_secretsList",
1982
+ "planScroll": "_367UGW_planScroll",
1983
+ "progressBlock": "_367UGW_progressBlock",
1984
+ "formError": "_367UGW_formError",
2023
1985
  "radioLabel": "_367UGW_radioLabel",
2024
- "dshCmSpin": "_367UGW_dshCmSpin",
2025
- "spinnerLabel": "_367UGW_spinnerLabel",
2026
- "pathIssueKind": "_367UGW_pathIssueKind",
2027
- "statRow": "_367UGW_statRow",
2028
- "progressBarDone": "_367UGW_progressBarDone",
2029
- "snapshotRowHeader": "_367UGW_snapshotRowHeader",
2030
- "badgeError": "_367UGW_badgeError",
2031
- "banner": "_367UGW_banner",
2032
- "modeHint": "_367UGW_modeHint",
2033
- "sectionTitleBlock": "_367UGW_sectionTitleBlock",
2034
- "primaryButton": "_367UGW_primaryButton",
2035
- "categoryItem": "_367UGW_categoryItem",
1986
+ "badge": "_367UGW_badge",
1987
+ "categoryDesc": "_367UGW_categoryDesc",
1988
+ "dangerButton": "_367UGW_dangerButton",
1989
+ "progressPercent": "_367UGW_progressPercent",
2036
1990
  "progressBar": "_367UGW_progressBar",
2037
- "conflictHead": "_367UGW_conflictHead",
2038
- "conflictDetail": "_367UGW_conflictDetail",
2039
- "conflictOptions": "_367UGW_conflictOptions",
1991
+ "ghostButton": "_367UGW_ghostButton",
1992
+ "errorBanner": "_367UGW_errorBanner",
1993
+ "categoryItem": "_367UGW_categoryItem",
1994
+ "errorList": "_367UGW_errorList",
1995
+ "reportText": "_367UGW_reportText",
1996
+ "conflictList": "_367UGW_conflictList",
1997
+ "errorReason": "_367UGW_errorReason",
1998
+ "severityError": "_367UGW_severityError",
1999
+ "rowActions": "_367UGW_rowActions",
2000
+ "rollbackBox": "_367UGW_rollbackBox",
2001
+ "badgeError": "_367UGW_badgeError",
2002
+ "badgeInfo": "_367UGW_badgeInfo",
2003
+ "optionsCard": "_367UGW_optionsCard",
2004
+ "dshCmSpin": "_367UGW_dshCmSpin",
2005
+ "input": "_367UGW_input",
2006
+ "errorItem": "_367UGW_errorItem",
2007
+ "groupHeader": "_367UGW_groupHeader",
2040
2008
  "spinnerWrap": "_367UGW_spinnerWrap",
2041
- "warnText": "_367UGW_warnText",
2042
- "planScroll": "_367UGW_planScroll",
2043
- "dshCmIndeterminate": "_367UGW_dshCmIndeterminate",
2009
+ "pathOld": "_367UGW_pathOld",
2010
+ "snapshotList": "_367UGW_snapshotList",
2011
+ "viewTab": "_367UGW_viewTab",
2012
+ "sectionSubtitle": "_367UGW_sectionSubtitle",
2044
2013
  "sectionBody": "_367UGW_sectionBody",
2045
- "groupCard": "_367UGW_groupCard",
2014
+ "actionRow": "_367UGW_actionRow",
2015
+ "modeHint": "_367UGW_modeHint",
2016
+ "statRow": "_367UGW_statRow",
2017
+ "categoryName": "_367UGW_categoryName",
2018
+ "progressTrack": "_367UGW_progressTrack",
2046
2019
  "reportFooter": "_367UGW_reportFooter",
2047
- "rollbackBox": "_367UGW_rollbackBox",
2048
- "errorList": "_367UGW_errorList",
2049
- "badgeOk": "_367UGW_badgeOk"
2020
+ "progressIndeterminate": "_367UGW_progressIndeterminate",
2021
+ "sectionHeader": "_367UGW_sectionHeader",
2022
+ "badgeWarn": "_367UGW_badgeWarn",
2023
+ "conflictHead": "_367UGW_conflictHead",
2024
+ "conflictOptions": "_367UGW_conflictOptions",
2025
+ "viewBody": "_367UGW_viewBody",
2026
+ "pathIssueKind": "_367UGW_pathIssueKind",
2027
+ "snapshotRowHeader": "_367UGW_snapshotRowHeader",
2028
+ "snapshotRow": "_367UGW_snapshotRow",
2029
+ "reportList": "_367UGW_reportList",
2030
+ "empty": "_367UGW_empty",
2031
+ "field": "_367UGW_field",
2032
+ "sectionTitle": "_367UGW_sectionTitle",
2033
+ "errorFooter": "_367UGW_errorFooter",
2034
+ "pathMappingList": "_367UGW_pathMappingList",
2035
+ "reportView": "_367UGW_reportView",
2036
+ "secretsList": "_367UGW_secretsList",
2037
+ "pathRow": "_367UGW_pathRow",
2038
+ "groupNote": "_367UGW_groupNote",
2039
+ "card": "_367UGW_card",
2040
+ "sectionTitleBlock": "_367UGW_sectionTitleBlock",
2041
+ "conflictItem": "_367UGW_conflictItem",
2042
+ "modeTabs": "_367UGW_modeTabs",
2043
+ "viewTabs": "_367UGW_viewTabs",
2044
+ "errorTitle": "_367UGW_errorTitle",
2045
+ "errorLine": "_367UGW_errorLine",
2046
+ "pathNew": "_367UGW_pathNew",
2047
+ "progressLabel": "_367UGW_progressLabel",
2048
+ "hint": "_367UGW_hint",
2049
+ "errorActionLabel": "_367UGW_errorActionLabel"
2050
2050
  };
2051
2051
  //#endregion
2052
2052
  //#region src/client/common/ui.tsx
@@ -3802,7 +3802,10 @@ const SYNC_API = {
3802
3802
  pull: "/api/dsh-config-manager/sync/pull",
3803
3803
  githubStart: "/api/dsh-config-manager/sync/github/start",
3804
3804
  githubPoll: "/api/dsh-config-manager/sync/github/poll",
3805
- githubCancel: "/api/dsh-config-manager/sync/github/cancel"
3805
+ githubCancel: "/api/dsh-config-manager/sync/github/cancel",
3806
+ history: "/api/dsh-config-manager/sync/history",
3807
+ apply: "/api/dsh-config-manager/sync/apply",
3808
+ rollback: "/api/dsh-config-manager/sync/rollback"
3806
3809
  };
3807
3810
  /** DSH credentials 中的同步 token 引用名(Host 半同值;仅供提示文案使用,值由 Host 读写) */
3808
3811
  const SYNC_CREDENTIAL_REF = "DSH_CONFIG_MANAGER_SYNC_TOKEN";
@@ -3869,6 +3872,18 @@ var SyncApi = class {
3869
3872
  async githubCancel(flowId) {
3870
3873
  return postJson(SYNC_API.githubCancel, { flowId }, this.t);
3871
3874
  }
3875
+ /** 同步历史:列出 localSnapshotsDir 下的快照目录(manifest.json: id/createdAt/sectionHashes) */
3876
+ async history() {
3877
+ return readJson(await fetch(SYNC_API.history), this.t);
3878
+ }
3879
+ /** 应用自动应用计划:写本地 + backup + (失败时) rollback + enqueue review;返回 ApplyReport */
3880
+ async apply(payload) {
3881
+ return postJson(SYNC_API.apply, payload, this.t);
3882
+ }
3883
+ /** 一键回滚:按 restoreId 调用 backup→rollback */
3884
+ async rollback(payload) {
3885
+ return postJson(SYNC_API.rollback, payload, this.t);
3886
+ }
3872
3887
  };
3873
3888
  //#endregion
3874
3889
  //#region src/client/sync/sync-view.ts
@@ -3966,11 +3981,11 @@ function computeSyncStatus(statusInfo, loading, error, t = zhUiT) {
3966
3981
  };
3967
3982
  return {
3968
3983
  kind: "ready",
3969
- text: `${statusInfo.credentialConfigured ? t("sync.credConfigured") : t("sync.credMissing")} · ${statusInfo.lastSyncAt !== void 0 && statusInfo.lastSyncAt !== "" ? t("sync.lastSync", { time: formatDateTime(statusInfo.lastSyncAt) }) : t("sync.neverSynced")}${statusInfo.transport !== void 0 ? ` · ${statusInfo.transport.type}${statusInfo.transport.ref !== "" ? `/${statusInfo.transport.ref}` : ""}` : ""}`
3984
+ text: `${statusInfo.credentialConfigured ? t("sync.credConfigured") : t("sync.credMissing")} · ${statusInfo.lastSyncAt !== void 0 && statusInfo.lastSyncAt !== "" ? t("sync.lastSync", { time: formatDateTime$1(statusInfo.lastSyncAt) }) : t("sync.neverSynced")}${statusInfo.transport !== void 0 ? ` · ${statusInfo.transport.type}${statusInfo.transport.ref !== "" ? `/${statusInfo.transport.ref}` : ""}` : ""}`
3970
3985
  };
3971
3986
  }
3972
3987
  /** ISO-8601 → 本地可读时间(YYYY-MM-DD HH:mm;非法输入原样返回) */
3973
- function formatDateTime(iso) {
3988
+ function formatDateTime$1(iso) {
3974
3989
  const d = new Date(iso);
3975
3990
  if (Number.isNaN(d.getTime())) return iso;
3976
3991
  const pad = (n) => String(n).padStart(2, "0");
@@ -4066,6 +4081,88 @@ function githubPollMessage(poll, t = zhUiT) {
4066
4081
  }
4067
4082
  }
4068
4083
  //#endregion
4084
+ //#region src/client/sync/history-model.ts
4085
+ /** 把 entries 排序(createdAt 倒序)并组装展示字段。 */
4086
+ function projectHistoryRows(entries) {
4087
+ return [...entries].sort((a, b) => a.createdAt < b.createdAt ? 1 : a.createdAt > b.createdAt ? -1 : 0);
4088
+ }
4089
+ /** ISO 时间 → 本地可读字符串(短格式) */
4090
+ function formatDateTime(iso) {
4091
+ if (!iso) return "—";
4092
+ const d = new Date(iso);
4093
+ if (isNaN(d.getTime())) return iso;
4094
+ const pad = (n) => String(n).padStart(2, "0");
4095
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
4096
+ }
4097
+ //#endregion
4098
+ //#region src/client/sync/SyncHistoryView.tsx
4099
+ /**
4100
+ * 同步历史视图(M6,P2b):列出 localSnapshotsDir 各快照目录的 manifest.json
4101
+ * (id + createdAt + sectionHashes);每行显示:快照 ID、时间、分区数、关联待审数(来自 sync-review-queue.json)。
4102
+ *
4103
+ * 数据获取:通过 Host 的 IPC 端点(与 SyncSettingsView 同模式)。本组件只负责渲染;
4104
+ * 数据加载由宿主 API 提供(不在浏览器侧直接读 fs)。
4105
+ */
4106
+ function SyncHistoryView(props) {
4107
+ const { api } = props;
4108
+ const [loading, setLoading] = (0, react.useState)(true);
4109
+ const [error, setError] = (0, react.useState)(null);
4110
+ const [entries, setEntries] = (0, react.useState)([]);
4111
+ (0, react.useEffect)(() => {
4112
+ let cancelled = false;
4113
+ (async () => {
4114
+ try {
4115
+ const data = await api.history();
4116
+ if (!cancelled) {
4117
+ setEntries(data);
4118
+ setLoading(false);
4119
+ }
4120
+ } catch (err) {
4121
+ if (!cancelled) {
4122
+ setError(err instanceof Error ? err.message : String(err));
4123
+ setLoading(false);
4124
+ }
4125
+ }
4126
+ })();
4127
+ return () => {
4128
+ cancelled = true;
4129
+ };
4130
+ }, [api]);
4131
+ const rows = (0, react.useMemo)(() => projectHistoryRows(entries), [entries]);
4132
+ if (loading) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Spinner, { label: "加载同步历史…" });
4133
+ if (error) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Card, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4134
+ className: "error",
4135
+ children: error
4136
+ }) });
4137
+ if (rows.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Card, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "尚无同步历史" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: "完成首次 push/pull 后这里会显示快照记录。" })] });
4138
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Card, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SectionTitle, { title: `同步历史(${rows.length})` }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
4139
+ className: "sync-history-table",
4140
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
4141
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: "快照 ID" }),
4142
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: "时间" }),
4143
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: "分区" }),
4144
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: "待审" })
4145
+ ] }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: rows.map((r) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
4146
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: r.id }) }),
4147
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: formatDateTime(r.createdAt) }),
4148
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: r.sectionCount }),
4149
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: r.reviewCount > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: r.reviewCount }) : 0 })
4150
+ ] }, r.id)) })]
4151
+ })] });
4152
+ }
4153
+ //#endregion
4154
+ //#region src/client/sync/SyncPullPreviewView.tsx
4155
+ /**
4156
+ * 拉取预览页(M5,P2b):消费 SyncApplyPlan,渲染
4157
+ * - 自动已应用列表(autoApplied)—— 绿色状态徽章 + sectionId/description;
4158
+ * - 待审变更列表(review)—— 每项三个按钮(用本地 / 用远端 / 跳过)+ 局部差异 diff;
4159
+ * - 整体 rollback 后「回滚到应用前」按钮(调用 restoreId);
4160
+ * - 全部 review 解决 → 触发 onAllResolved 回调(sync 完成)。
4161
+ *
4162
+ * 设计:与 SyncSettingsView 同模式 —— 全部渲染模型来自 ./sync-view.ts 纯函数,
4163
+ * 组件只做装配 + 状态;测试在 node --test 跑纯函数部分。
4164
+ */
4165
+ //#endregion
4069
4166
  //#region src/client/sync/SyncSettingsView.tsx
4070
4167
  /**
4071
4168
  * 远程同步面板(备份与迁移页的第 4 个 tab 内容)。
@@ -4102,6 +4199,7 @@ const initial = {
4102
4199
  busy: null,
4103
4200
  pushReport: null,
4104
4201
  pullReport: null,
4202
+ applyReport: null,
4105
4203
  error: null,
4106
4204
  github: initialGithub
4107
4205
  };
@@ -4267,6 +4365,45 @@ function SyncSettingsView({ api, t }) {
4267
4365
  });
4268
4366
  }
4269
4367
  };
4368
+ /** P2:自动应用(Host 端 engine.merge + classifyMergePlan + applyMergePlan 走完整链路) */
4369
+ const runApply = async () => {
4370
+ patch({
4371
+ busy: "apply",
4372
+ error: null,
4373
+ applyReport: null
4374
+ });
4375
+ try {
4376
+ const report = await api.apply(payload());
4377
+ patch({
4378
+ busy: null,
4379
+ applyReport: report
4380
+ });
4381
+ } catch (err) {
4382
+ patch({
4383
+ busy: null,
4384
+ error: err instanceof Error ? err.message : String(err)
4385
+ });
4386
+ }
4387
+ };
4388
+ /** P2:一键回滚(按 apply 返回的 restoreId 调 Host /sync/rollback) */
4389
+ const runRollback = async (restoreId) => {
4390
+ patch({
4391
+ busy: "rollback",
4392
+ error: null
4393
+ });
4394
+ try {
4395
+ await api.rollback({ restoreId });
4396
+ patch({
4397
+ busy: null,
4398
+ applyReport: null
4399
+ });
4400
+ } catch (err) {
4401
+ patch({
4402
+ busy: null,
4403
+ error: err instanceof Error ? err.message : String(err)
4404
+ });
4405
+ }
4406
+ };
4270
4407
  const status = computeSyncStatus(state.statusInfo, state.loading, state.loadError, uiT);
4271
4408
  const buttons = computeSyncButtons(state.busy, state.repoUrl, uiT);
4272
4409
  const pushView = pushReportView(state.pushReport, uiT);
@@ -4546,7 +4683,65 @@ function SyncSettingsView({ api, t }) {
4546
4683
  kind: "info",
4547
4684
  children: pullView.previewHint
4548
4685
  })
4549
- ] })
4686
+ ] }),
4687
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4688
+ className: config_manager_module_css_default.actionRow,
4689
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
4690
+ variant: "primary",
4691
+ disabled: state.busy !== null || state.pullReport === null,
4692
+ onClick: () => {
4693
+ runApply();
4694
+ },
4695
+ children: state.busy === "apply" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Spinner, { label: "自动应用中…" }) : "自动应用(合并 + 写本地)"
4696
+ })
4697
+ }),
4698
+ state.applyReport !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Card, { children: [
4699
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4700
+ className: config_manager_module_css_default.groupLabel,
4701
+ children: "自动应用结果"
4702
+ }),
4703
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Banner, {
4704
+ kind: state.applyReport.ok ? "ok" : "error",
4705
+ children: state.applyReport.ok ? `已应用 ${state.applyReport.applied.length} 个分区(restoreId=${state.applyReport.restoreId})` : `自动应用失败(已整体回滚,restoreId=${state.applyReport.restoreId})`
4706
+ }),
4707
+ state.applyReport.applied.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4708
+ className: config_manager_module_css_default.fieldLabel,
4709
+ children: "已写入"
4710
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4711
+ className: config_manager_module_css_default.statRow,
4712
+ children: state.applyReport.applied.map((sid) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
4713
+ kind: "ok",
4714
+ children: sid
4715
+ }, sid))
4716
+ })] }),
4717
+ state.applyReport.warnings.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4718
+ className: config_manager_module_css_default.fieldLabel,
4719
+ children: "告警"
4720
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
4721
+ className: config_manager_module_css_default.warnList,
4722
+ children: state.applyReport.warnings.map((w, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: w }, i))
4723
+ })] }),
4724
+ state.applyReport.review.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4725
+ className: config_manager_module_css_default.fieldLabel,
4726
+ children: "待审(已入 sync-review-queue.json)"
4727
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
4728
+ className: config_manager_module_css_default.warnList,
4729
+ children: state.applyReport.review.map((r, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
4730
+ r.sectionId,
4731
+ " — ",
4732
+ r.description
4733
+ ] }, i))
4734
+ })] }),
4735
+ !state.applyReport.ok && state.applyReport.restoreId !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
4736
+ variant: "danger",
4737
+ disabled: state.busy !== null,
4738
+ onClick: () => {
4739
+ runRollback(state.applyReport.restoreId);
4740
+ },
4741
+ children: state.busy === "rollback" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Spinner, { label: "回滚中…" }) : "回滚到应用前"
4742
+ })
4743
+ ] }),
4744
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SyncHistoryView, { api })
4550
4745
  ]
4551
4746
  });
4552
4747
  }