dsh-config-manager 0.1.19 → 0.1.21

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 (82) hide show
  1. package/lib/client.d.ts +319 -2
  2. package/lib/client.js +1271 -109
  3. package/lib/client.js.map +1 -1
  4. package/lib/core/run-registry.d.ts +2 -2
  5. package/lib/core/run-registry.js +5 -1
  6. package/lib/core/run-registry.js.map +1 -1
  7. package/lib/index.js +408 -2
  8. package/lib/index.js.map +1 -1
  9. package/lib/sync/ancestor.d.ts +17 -0
  10. package/lib/sync/ancestor.js +64 -0
  11. package/lib/sync/ancestor.js.map +1 -0
  12. package/lib/sync/autosync-config.d.ts +34 -0
  13. package/lib/sync/autosync-config.js +116 -0
  14. package/lib/sync/autosync-config.js.map +1 -0
  15. package/lib/sync/autosync-scheduler.d.ts +95 -0
  16. package/lib/sync/autosync-scheduler.js +379 -0
  17. package/lib/sync/autosync-scheduler.js.map +1 -0
  18. package/lib/sync/merge.d.ts +42 -0
  19. package/lib/sync/merge.js +325 -0
  20. package/lib/sync/merge.js.map +1 -0
  21. package/lib/sync/review-queue.d.ts +51 -0
  22. package/lib/sync/review-queue.js +119 -0
  23. package/lib/sync/review-queue.js.map +1 -0
  24. package/lib/sync/risk.d.ts +59 -0
  25. package/lib/sync/risk.js +76 -0
  26. package/lib/sync/risk.js.map +1 -0
  27. package/lib/sync/sync-config.d.ts +7 -2
  28. package/lib/sync/sync-config.js +24 -4
  29. package/lib/sync/sync-config.js.map +1 -1
  30. package/lib/sync/sync-engine.d.ts +88 -2
  31. package/lib/sync/sync-engine.js +333 -8
  32. package/lib/sync/sync-engine.js.map +1 -1
  33. package/lib/sync/sync-history.d.ts +39 -0
  34. package/lib/sync/sync-history.js +88 -0
  35. package/lib/sync/sync-history.js.map +1 -0
  36. package/lib/sync/sync-session.d.ts +39 -0
  37. package/lib/sync/sync-session.js +54 -0
  38. package/lib/sync/sync-session.js.map +1 -0
  39. package/lib/sync/sync-state.d.ts +7 -2
  40. package/lib/sync/sync-state.js +9 -5
  41. package/lib/sync/sync-state.js.map +1 -1
  42. package/lib/ui/i18n.d.ts +63 -0
  43. package/lib/ui/i18n.js +130 -1
  44. package/lib/ui/i18n.js.map +1 -1
  45. package/package.json +1 -1
  46. package/src/client/config-manager.module.css +18 -0
  47. package/src/client/sync/SyncConfirmView.tsx +301 -0
  48. package/src/client/sync/SyncHistoryView.test.ts +40 -0
  49. package/src/client/sync/SyncHistoryView.tsx +155 -0
  50. package/src/client/sync/SyncSettingsView.tsx +241 -12
  51. package/src/client/sync/history-model.test.ts +82 -0
  52. package/src/client/sync/history-model.ts +112 -0
  53. package/src/client/sync/sync-api.test.ts +155 -0
  54. package/src/client/sync/sync-api.ts +219 -0
  55. package/src/client/sync/sync-locales.ts +162 -1
  56. package/src/client/sync/sync-view-v2.test.ts +131 -0
  57. package/src/client/sync/sync-view.ts +151 -4
  58. package/src/core/run-registry.ts +7 -3
  59. package/src/index.ts +420 -3
  60. package/src/sync/ancestor.test.ts +152 -0
  61. package/src/sync/ancestor.ts +81 -0
  62. package/src/sync/autosync-config.test.ts +93 -0
  63. package/src/sync/autosync-config.ts +137 -0
  64. package/src/sync/autosync-scheduler.test.ts +191 -0
  65. package/src/sync/autosync-scheduler.ts +443 -0
  66. package/src/sync/merge.test.ts +204 -0
  67. package/src/sync/merge.ts +360 -0
  68. package/src/sync/review-queue.test.ts +120 -0
  69. package/src/sync/review-queue.ts +167 -0
  70. package/src/sync/risk.test.ts +131 -0
  71. package/src/sync/risk.ts +122 -0
  72. package/src/sync/sync-config.test.ts +106 -0
  73. package/src/sync/sync-config.ts +23 -4
  74. package/src/sync/sync-engine.test.ts +392 -3
  75. package/src/sync/sync-engine.ts +383 -10
  76. package/src/sync/sync-history.test.ts +85 -0
  77. package/src/sync/sync-history.ts +126 -0
  78. package/src/sync/sync-session.test.ts +137 -0
  79. package/src/sync/sync-session.ts +76 -0
  80. package/src/sync/sync-state.test.ts +48 -2
  81. package/src/sync/sync-state.ts +11 -5
  82. package/src/ui/i18n.ts +132 -3
package/lib/client.js CHANGED
@@ -160,6 +160,69 @@ const uiZh = {
160
160
  "sync.pullOk": "远端快照 {id} 差异预览:共 {count} 项变更",
161
161
  "sync.pullEmpty": "远端快照与本地一致(无变更)",
162
162
  "sync.previewHint": "以上为只读差异预览,不会执行导入。v1 暂不提供一键导入接线;如需应用远端配置,请使用「导入恢复」向导手动导入导出的备份。",
163
+ "sync.syncTitle": "一键同步",
164
+ "sync.syncButton": "一键同步",
165
+ "sync.syncing": "正在同步…",
166
+ "sync.syncHeadline": "远端快照 {id} 差异确认:共 {count} 项",
167
+ "sync.syncEmpty": "远端快照与本地一致(无变更)",
168
+ "sync.syncFailed": "同步失败",
169
+ "sync.confirmAdopt": "采用远端",
170
+ "sync.confirmSkip": "跳过",
171
+ "sync.confirmRequired": "需要人工决策",
172
+ "sync.needsReviewBadge": "需人工决策",
173
+ "sync.selectSnapshot": "选择历史快照",
174
+ "sync.latestSnapshot": "最新快照",
175
+ "sync.noSnapshots": "远端暂无快照",
176
+ "sync.confirmImport": "确认导入",
177
+ "sync.cancelConfirm": "取消",
178
+ "sync.conflictResolve": "冲突解决",
179
+ "sync.conflictUseLocal": "用本地",
180
+ "sync.conflictUseRemote": "用远端",
181
+ "sync.conflictSkip": "跳过",
182
+ "sync.conflictResolvedLocal": "已选:本地",
183
+ "sync.conflictResolvedRemote": "已选:远端",
184
+ "sync.conflictResolvedSkip": "已选:跳过",
185
+ "sync.importDone": "已导入 {n} 个分区",
186
+ "sync.importSkipped": "已跳过 {n} 项",
187
+ "sync.importFailed": "导入失败(已整体回滚)",
188
+ "sync.rollbackButton": "回滚到应用前",
189
+ "sync.rollingBack": "正在回滚…",
190
+ "sync.rollbackDone": "已回滚到应用前",
191
+ "sync.appliedSections": "已写入",
192
+ "sync.importWarnings": "导入告警",
193
+ "sync.autosyncTitle": "自动同步",
194
+ "sync.autosyncDesc": "开启后,DSH 将按固定间隔自动执行双向同步(拉取合并 + 上传);DSH 启动时还会触发一次「自动下载合并(不上传)」以保持本地为最新。仅在无冲突且无需人工干预时才自动写入。",
195
+ "sync.autosyncEnabled": "启用自动同步",
196
+ "sync.autosyncInterval": "同步间隔",
197
+ "sync.autosyncIntervalHint": "DSH 启动时自动下载合并不上传。",
198
+ "sync.autosyncNever": "从未运行",
199
+ "sync.autosyncLastRun": "上次运行:{time}",
200
+ "sync.autosyncNextIn": "下次约 {time} 后",
201
+ "sync.autosyncSuccess": "成功",
202
+ "sync.autosyncSkipped": "已跳过",
203
+ "sync.autosyncFailed": "失败",
204
+ "sync.autosyncPartial": "部分成功",
205
+ "sync.autosyncFailCount": "连续失败 {n} 次",
206
+ "sync.autosyncSyncing": "自动同步进行中…",
207
+ "sync.interval.5m": "5 分钟",
208
+ "sync.interval.15m": "15 分钟",
209
+ "sync.interval.30m": "30 分钟",
210
+ "sync.interval.60m": "60 分钟",
211
+ "sync.interval.6h": "6 小时",
212
+ "sync.interval.12h": "12 小时",
213
+ "sync.interval.24h": "24 小时",
214
+ "sync.hist.autosync": "自动同步",
215
+ "sync.hist.directionPull": "下载",
216
+ "sync.hist.directionPush": "上传",
217
+ "sync.hist.directionBoth": "双向",
218
+ "sync.hist.skipReasonConflict": "冲突项被跳过",
219
+ "sync.hist.skipReasonNoRemote": "远端无快照",
220
+ "sync.hist.skipReasonNotConfigured": "未配置仓库",
221
+ "sync.hist.skipReasonNetwork": "网络问题",
222
+ "sync.hist.conflictedSections": "跳过冲突分区:{sections}",
223
+ "sync.hist.appliedSections": "应用分区:{sections}",
224
+ "sync.hist.failedError": "错误:{error}",
225
+ "sync.hist.notifiedAt": "已通知",
163
226
  "sync.github.starting": "正在发起 GitHub 授权…",
164
227
  "sync.github.waitingNoCode": "请在浏览器中打开授权页面并完成授权…",
165
228
  "sync.github.waiting": "请在浏览器中打开授权页面,输入一次性代码 {code} 完成授权(自动等待确认)。",
@@ -316,7 +379,70 @@ const uiEn = {
316
379
  "sync.pullOk": "Remote snapshot {id} diff preview: {count} change(s)",
317
380
  "sync.pullEmpty": "Remote snapshot matches local (no changes)",
318
381
  "sync.previewHint": "Read-only diff preview above; nothing is imported. v1 does not wire one-click import yet — use the Import wizard to apply a downloaded backup if needed.",
319
- "sync.github.starting": "Starting GitHub authorization…",
382
+ "sync.syncTitle": "One-Click Sync",
383
+ "sync.syncButton": "One-click sync",
384
+ "sync.syncing": "Syncing…",
385
+ "sync.syncHeadline": "Remote snapshot {id} diff confirm: {count} item(s)",
386
+ "sync.syncEmpty": "Remote snapshot matches local (no changes)",
387
+ "sync.syncFailed": "Sync failed",
388
+ "sync.confirmAdopt": "Adopt remote",
389
+ "sync.confirmSkip": "Skip",
390
+ "sync.confirmRequired": "Needs decision",
391
+ "sync.needsReviewBadge": "Needs decision",
392
+ "sync.selectSnapshot": "Select snapshot",
393
+ "sync.latestSnapshot": "Latest snapshot",
394
+ "sync.noSnapshots": "No remote snapshots",
395
+ "sync.confirmImport": "Confirm import",
396
+ "sync.cancelConfirm": "Cancel",
397
+ "sync.conflictResolve": "Resolve conflict",
398
+ "sync.conflictUseLocal": "Use local",
399
+ "sync.conflictUseRemote": "Use remote",
400
+ "sync.conflictSkip": "Skip",
401
+ "sync.conflictResolvedLocal": "Chosen: local",
402
+ "sync.conflictResolvedRemote": "Chosen: remote",
403
+ "sync.conflictResolvedSkip": "Chosen: skip",
404
+ "sync.importDone": "Imported {n} section(s)",
405
+ "sync.importSkipped": "Skipped {n} item(s)",
406
+ "sync.importFailed": "Import failed (rolled back)",
407
+ "sync.rollbackButton": "Rollback",
408
+ "sync.rollingBack": "Rolling back…",
409
+ "sync.rollbackDone": "Rolled back",
410
+ "sync.appliedSections": "Written",
411
+ "sync.importWarnings": "Import warnings",
412
+ "sync.autosyncTitle": "Auto Sync",
413
+ "sync.autosyncDesc": "When enabled, DSH will run two-way sync (pull merge + push) on a fixed interval; on startup it also triggers a download-merge (no upload) to keep local up to date. Local writes happen only when there are no conflicts or manual-decision items.",
414
+ "sync.autosyncEnabled": "Enable auto sync",
415
+ "sync.autosyncInterval": "Sync interval",
416
+ "sync.autosyncIntervalHint": "Auto download-merge on DSH startup, no upload.",
417
+ "sync.autosyncNever": "Never run",
418
+ "sync.autosyncLastRun": "Last run: {time}",
419
+ "sync.autosyncNextIn": "Next in ~{time}",
420
+ "sync.autosyncSuccess": "Success",
421
+ "sync.autosyncSkipped": "Skipped",
422
+ "sync.autosyncFailed": "Failed",
423
+ "sync.autosyncPartial": "Partial",
424
+ "sync.autosyncFailCount": "{n} consecutive failure(s)",
425
+ "sync.autosyncSyncing": "Auto sync running…",
426
+ "sync.interval.5m": "5 min",
427
+ "sync.interval.15m": "15 min",
428
+ "sync.interval.30m": "30 min",
429
+ "sync.interval.60m": "60 min",
430
+ "sync.interval.6h": "6 hr",
431
+ "sync.interval.12h": "12 hr",
432
+ "sync.interval.24h": "24 hr",
433
+ "sync.hist.autosync": "Auto Sync",
434
+ "sync.hist.directionPull": "Pull",
435
+ "sync.hist.directionPush": "Push",
436
+ "sync.hist.directionBoth": "Both",
437
+ "sync.hist.skipReasonConflict": "Conflict items skipped",
438
+ "sync.hist.skipReasonNoRemote": "No remote snapshot",
439
+ "sync.hist.skipReasonNotConfigured": "No repository configured",
440
+ "sync.hist.skipReasonNetwork": "Network issue",
441
+ "sync.hist.conflictedSections": "Skipped conflict sections: {sections}",
442
+ "sync.hist.appliedSections": "Applied sections: {sections}",
443
+ "sync.hist.failedError": "Error: {error}",
444
+ "sync.hist.notifiedAt": "Notified",
445
+ "sync.github.starting": "Starting GitHub a​u​t​h​o​r​i​z​a​t​i​o​n​…",
320
446
  "sync.github.waitingNoCode": "Open the authorization page in your browser and complete the flow…",
321
447
  "sync.github.waiting": "Open the authorization page in your browser, enter the one-time code {code} to finish (waiting for confirmation).",
322
448
  "sync.github.polling": "Confirming GitHub authorization…",
@@ -1939,7 +2065,7 @@ var RunStore = class {
1939
2065
  const runStore = new RunStore();
1940
2066
  //#endregion
1941
2067
  //#region \0config-manager-css:/home/runner/work/dsh-config-manager/dsh-config-manager/src/client/config-manager.module.css.js
1942
- const css = "._367UGW_section{height:100%;min-height:0;color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family);flex-direction:column;gap:10px;font-size:13px;display:flex}._367UGW_sectionHeader{flex:none;align-items:center;gap:10px;display:flex}._367UGW_viewTabs{border-bottom:1px solid var(--dsw-alias-border-l1);flex:1;gap:2px;display:flex}._367UGW_viewTab{color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;background:0 0;border:none;border-bottom:2px solid #0000;border-radius:6px 6px 0 0;padding:7px 14px;font-size:13px}._367UGW_viewTab:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}._367UGW_viewTab[data-active]{color:var(--dsw-alias-label-primary);border-bottom-color:var(--dsw-alias-state-business-primary);font-weight:600}._367UGW_sectionBody{flex-direction:column;flex:1;min-height:0;display:flex;overflow-y:auto}._367UGW_viewBody{flex-direction:column;gap:12px;padding:4px 2px 16px;display:flex}._367UGW_sectionTitleBlock{flex-direction:column;gap:2px;display:flex}._367UGW_sectionTitle{color:var(--dsw-alias-label-primary);margin:0;font-size:15px;font-weight:700}._367UGW_sectionSubtitle{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px;line-height:1.5}._367UGW_primaryButton{color:var(--dsw-alias-label-primary-foreground);background:var(--dsw-alias-button-info-fill);cursor:pointer;white-space:nowrap;border:none;border-radius:8px;align-items:center;gap:6px;padding:6px 14px;font-size:13px;font-weight:600;display:inline-flex}._367UGW_primaryButton:hover:not(:disabled){background:var(--dsw-alias-button-info-hover)}._367UGW_primaryButton:disabled{opacity:.5;cursor:default}._367UGW_ghostButton{color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);cursor:pointer;white-space:nowrap;background:0 0;border-radius:8px;align-items:center;gap:6px;padding:5px 12px;font-size:12px;display:inline-flex}._367UGW_ghostButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}._367UGW_ghostButton:disabled{opacity:.45;cursor:default}._367UGW_dangerButton{color:var(--dsw-alias-state-error-primary);border:1px solid var(--dsw-alias-state-error-primary);cursor:pointer;white-space:nowrap;background:0 0;border-radius:8px;padding:5px 12px;font-size:12px}._367UGW_dangerButton:hover:not(:disabled){background:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-label-primary-foreground)}._367UGW_dangerButton:disabled{opacity:.45;cursor:default}._367UGW_badge{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);white-space:nowrap;border-radius:999px;padding:1px 8px;font-size:11px;line-height:1.6;display:inline-block}._367UGW_badgeInfo{color:var(--dsw-alias-state-business-primary);border-color:var(--dsw-alias-state-business-primary)}._367UGW_badgeOk{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary)}._367UGW_badgeWarn{color:var(--dsw-alias-state-warn-primary);border-color:var(--dsw-alias-state-warn-primary)}._367UGW_badgeError{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}._367UGW_banner{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere;border-radius:8px;padding:8px 12px;font-size:12.5px;line-height:1.5}._367UGW_banner[data-kind=ok]{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary)}._367UGW_banner[data-kind=error]{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}._367UGW_banner[data-kind=info]{color:var(--dsw-alias-state-business-primary);border-color:var(--dsw-alias-state-business-primary)}._367UGW_banner[data-kind=warn]{color:var(--dsw-alias-state-warn-primary);border-color:var(--dsw-alias-state-warn-primary)}._367UGW_card{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:10px;flex-direction:column;flex:none;gap:10px;padding:12px;display:flex}._367UGW_field{flex-direction:column;gap:5px;display:flex}._367UGW_fieldLabel{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600}._367UGW_input{color:var(--dsw-alias-label-primary);background:var(--dsw-specific-input-major);border:1px solid var(--dsw-alias-border-l2);resize:vertical;border-radius:8px;outline:none;padding:7px 10px;font-family:inherit;font-size:13px}._367UGW_input:focus{border-color:var(--dsw-alias-state-business-primary)}._367UGW_input::placeholder{color:var(--dsw-alias-label-tertiary)}._367UGW_input:disabled{opacity:.55}._367UGW_hint{color:var(--dsw-alias-label-tertiary);font-size:11.5px;line-height:1.5}._367UGW_formError{color:var(--dsw-alias-state-error-primary);margin:0;font-size:12px}._367UGW_checkboxRow{color:var(--dsw-alias-label-primary);cursor:pointer;align-items:flex-start;gap:8px;font-size:13px;display:flex}._367UGW_checkboxRow input{flex:none;margin-top:2px}._367UGW_radioLabel{color:var(--dsw-alias-label-primary);cursor:pointer;align-items:center;gap:6px;font-size:13px;display:inline-flex}._367UGW_secretFields{grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px 12px;padding-top:4px;display:grid}._367UGW_hiddenFile{display:none}._367UGW_modeTabs{border-bottom:1px solid var(--dsw-alias-border-l1);flex:none;gap:2px;display:flex}._367UGW_modeTab{color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-bottom:2px solid #0000;padding:6px 12px;font-size:12.5px}._367UGW_modeTab:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}._367UGW_modeTab[data-active]{color:var(--dsw-alias-label-primary);border-bottom-color:var(--dsw-alias-state-business-primary);font-weight:600}._367UGW_modeHint{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.5}._367UGW_groupList{flex-direction:column;gap:10px;display:flex}._367UGW_groupCard{gap:8px}._367UGW_groupHeader{flex-wrap:wrap;align-items:baseline;gap:8px;display:flex}._367UGW_groupLabel{color:var(--dsw-alias-label-primary);font-size:12.5px;font-weight:700}._367UGW_groupNote{color:var(--dsw-alias-label-tertiary);font-size:11.5px}._367UGW_groupItems{flex-direction:column;gap:6px;display:flex}._367UGW_categoryItem{flex-wrap:wrap;align-items:center;gap:8px;display:inline-flex}._367UGW_categoryName{font-weight:500}._367UGW_categoryDesc{color:var(--dsw-alias-label-tertiary);font-size:11.5px}._367UGW_optionsCard{gap:8px}._367UGW_warnList{margin:4px 0 0;padding-left:18px}._367UGW_actionRow{flex-wrap:wrap;align-items:center;gap:10px;display:flex}._367UGW_progressBlock{flex-direction:column;flex:none;gap:6px;display:flex}._367UGW_progressMeta{color:var(--dsw-alias-label-secondary);flex-wrap:wrap;gap:12px;font-size:12px;display:flex}._367UGW_progressLabel{font-weight:600}._367UGW_progressDetail{color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;max-width:320px;overflow:hidden}._367UGW_progressBadge{font-variant-numeric:tabular-nums;white-space:nowrap;border-radius:999px;align-items:center;gap:2px;padding:1px 8px;font-size:11px;line-height:18px;display:inline-flex}._367UGW_progressBadgeSection{background:color-mix(in srgb, var(--dsw-alias-state-business-primary) 14%, transparent);color:var(--dsw-alias-state-business-primary)}._367UGW_progressBadgeCount{background:color-mix(in srgb, var(--dsw-alias-state-info-primary) 14%, transparent);color:var(--dsw-alias-state-info-primary)}._367UGW_progressPercent{font-variant-numeric:tabular-nums;margin-left:auto}._367UGW_progressTrack{background:var(--dsw-alias-interactive-bg-hover);border-radius:999px;height:8px;overflow:hidden}._367UGW_progressBar{background:var(--dsw-alias-state-business-primary);border-radius:999px;height:100%;transition:width .12s linear}._367UGW_progressBarDone{background:var(--dsw-alias-state-success-primary)}._367UGW_progressIndeterminate{width:40%;animation:1s ease-in-out infinite _367UGW_dshCmIndeterminate}@keyframes _367UGW_dshCmIndeterminate{0%{margin-left:-40%}to{margin-left:100%}}._367UGW_reportView{flex-direction:column;gap:10px;display:flex}._367UGW_statRow{flex-wrap:wrap;align-items:center;gap:6px;display:flex}._367UGW_reportText{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);white-space:pre-wrap;word-break:break-all;max-height:260px;color:var(--dsw-alias-label-primary);border-radius:8px;margin:0;padding:10px 12px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11.5px;line-height:1.6;overflow:auto}._367UGW_reportFooter{justify-content:flex-end;gap:8px;display:flex}._367UGW_rollbackBox{border:1px solid var(--dsw-alias-state-warn-primary);color:var(--dsw-alias-state-warn-primary);border-radius:8px;flex-direction:column;gap:6px;padding:10px 12px;display:flex}._367UGW_rollbackBox ._367UGW_reportText{border-color:var(--dsw-alias-border-l2)}._367UGW_errorBanner{border:1px solid var(--dsw-alias-state-error-primary);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);border-radius:8px;flex-direction:column;gap:6px;padding:10px 12px;display:flex}._367UGW_errorTitle{color:var(--dsw-alias-state-error-primary);font-size:13px;font-weight:700}._367UGW_errorReason{white-space:pre-wrap;word-break:break-all;color:var(--dsw-alias-label-secondary);margin:0;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11.5px}._367UGW_errorAction{color:var(--dsw-alias-label-secondary);font-size:12.5px}._367UGW_errorActionLabel{color:var(--dsw-alias-state-business-primary)}._367UGW_errorItem{color:var(--dsw-alias-label-tertiary);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11.5px}._367UGW_errorFooter{justify-content:flex-end;display:flex}._367UGW_errorList{flex-direction:column;gap:4px;display:flex}._367UGW_errorLine{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);white-space:pre-wrap;word-break:break-all;color:var(--dsw-alias-state-error-primary);border-radius:6px;margin:0;padding:6px 10px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px}._367UGW_conflictList{flex-direction:column;gap:10px;display:flex}._367UGW_conflictItem{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;flex-direction:column;gap:6px;padding:10px 12px;display:flex}._367UGW_conflictHead{flex-wrap:wrap;align-items:center;gap:8px;display:flex}._367UGW_conflictId{color:var(--dsw-alias-label-primary);font-size:12.5px;font-weight:600}._367UGW_severityError{color:var(--dsw-alias-state-error-primary);border:1px solid var(--dsw-alias-state-error-primary);border-radius:999px;padding:0 6px;font-size:10.5px}._367UGW_conflictDetail{background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2);white-space:pre-wrap;word-break:break-all;color:var(--dsw-alias-label-secondary);border-radius:6px;margin:0;padding:6px 8px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px}._367UGW_conflictOptions{flex-wrap:wrap;gap:14px;display:flex}._367UGW_pathMappingList{flex-direction:column;gap:8px;display:flex}._367UGW_pathRow{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:10px;padding:10px 12px;display:grid}._367UGW_pathOld,._367UGW_pathNew{flex-direction:column;gap:4px;min-width:0;display:flex}._367UGW_pathValue{text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-primary);margin:0;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11.5px;overflow:hidden}._367UGW_pathIssueKind{color:var(--dsw-alias-state-warn-primary);font-size:10.5px}._367UGW_secretsList{flex-direction:column;gap:10px;display:flex}._367UGW_empty{text-align:center;color:var(--dsw-alias-label-tertiary);padding:22px 12px;font-size:12.5px}._367UGW_spinnerWrap{align-items:center;gap:6px;display:inline-flex}._367UGW_spinner{border:2px solid var(--dsw-alias-state-business-primary);vertical-align:-1px;border-top-color:#0000;border-radius:50%;flex:none;width:11px;height:11px;animation:.8s linear infinite _367UGW_dshCmSpin;display:inline-block}._367UGW_spinnerLabel{color:var(--dsw-alias-label-secondary);font-size:12px}@keyframes _367UGW_dshCmSpin{to{transform:rotate(360deg)}}._367UGW_snapshotList{flex-direction:column;gap:2px;display:flex}._367UGW_snapshotRowHeader,._367UGW_snapshotRow{grid-template-columns:minmax(120px,1.2fr) minmax(140px,1.6fr) 90px 56px 56px;align-items:center;gap:8px;padding:6px 10px;font-size:12px;display:grid}._367UGW_snapshotRowHeader{color:var(--dsw-alias-label-tertiary);border-bottom:1px solid var(--dsw-alias-border-l1);font-size:11px}._367UGW_snapshotRow{text-align:left;width:100%;color:var(--dsw-alias-label-primary);cursor:pointer;background:0 0;border:1px solid #0000;border-radius:6px}._367UGW_snapshotRow:hover{background:var(--dsw-alias-interactive-bg-hover)}._367UGW_snapshotRow[data-active]{border-color:var(--dsw-alias-state-business-primary);background:color-mix(in srgb, var(--dsw-alias-state-business-primary) 10%, transparent)}._367UGW_warnText{color:var(--dsw-alias-state-warn-primary)}._367UGW_reportList{color:var(--dsw-alias-label-primary);flex-direction:column;gap:3px;margin:8px 0 4px;padding-left:18px;font-size:12px;display:flex}._367UGW_kindTag{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary);border-radius:4px;padding:1px 6px;font-size:10.5px;display:inline-block}._367UGW_planScroll{overscroll-behavior:contain;background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;height:280px;padding:2px 10px 8px;overflow-y:auto}._367UGW_planScroll ._367UGW_reportList{margin-top:6px}._367UGW_reportScroll{overscroll-behavior:contain;background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;max-height:220px;padding:2px 10px 6px;overflow-y:auto}._367UGW_reportScroll ._367UGW_reportList{margin-top:4px}._367UGW_rowActions{gap:8px;margin-top:10px;display:flex}";
2068
+ const css = "._367UGW_section{height:100%;min-height:0;color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family);flex-direction:column;gap:10px;font-size:13px;display:flex}._367UGW_sectionHeader{flex:none;align-items:center;gap:10px;display:flex}._367UGW_viewTabs{border-bottom:1px solid var(--dsw-alias-border-l1);flex:1;gap:2px;display:flex}._367UGW_viewTab{color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;background:0 0;border:none;border-bottom:2px solid #0000;border-radius:6px 6px 0 0;padding:7px 14px;font-size:13px}._367UGW_viewTab:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}._367UGW_viewTab[data-active]{color:var(--dsw-alias-label-primary);border-bottom-color:var(--dsw-alias-state-business-primary);font-weight:600}._367UGW_sectionBody{flex-direction:column;flex:1;min-height:0;display:flex;overflow-y:auto}._367UGW_viewBody{flex-direction:column;gap:12px;padding:4px 2px 16px;display:flex}._367UGW_sectionTitleBlock{flex-direction:column;gap:2px;display:flex}._367UGW_sectionTitle{color:var(--dsw-alias-label-primary);margin:0;font-size:15px;font-weight:700}._367UGW_sectionSubtitle{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px;line-height:1.5}._367UGW_primaryButton{color:var(--dsw-alias-label-primary-foreground);background:var(--dsw-alias-button-info-fill);cursor:pointer;white-space:nowrap;border:none;border-radius:8px;align-items:center;gap:6px;padding:6px 14px;font-size:13px;font-weight:600;display:inline-flex}._367UGW_primaryButton:hover:not(:disabled){background:var(--dsw-alias-button-info-hover)}._367UGW_primaryButton:disabled{opacity:.5;cursor:default}._367UGW_ghostButton{color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);cursor:pointer;white-space:nowrap;background:0 0;border-radius:8px;align-items:center;gap:6px;padding:5px 12px;font-size:12px;display:inline-flex}._367UGW_ghostButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}._367UGW_ghostButton:disabled{opacity:.45;cursor:default}._367UGW_dangerButton{color:var(--dsw-alias-state-error-primary);border:1px solid var(--dsw-alias-state-error-primary);cursor:pointer;white-space:nowrap;background:0 0;border-radius:8px;padding:5px 12px;font-size:12px}._367UGW_dangerButton:hover:not(:disabled){background:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-label-primary-foreground)}._367UGW_dangerButton:disabled{opacity:.45;cursor:default}._367UGW_badge{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);white-space:nowrap;border-radius:999px;padding:1px 8px;font-size:11px;line-height:1.6;display:inline-block}._367UGW_badgeInfo{color:var(--dsw-alias-state-business-primary);border-color:var(--dsw-alias-state-business-primary)}._367UGW_badgeOk{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary)}._367UGW_badgeWarn{color:var(--dsw-alias-state-warn-primary);border-color:var(--dsw-alias-state-warn-primary)}._367UGW_badgeError{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}._367UGW_banner{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere;border-radius:8px;padding:8px 12px;font-size:12.5px;line-height:1.5}._367UGW_banner[data-kind=ok]{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary)}._367UGW_banner[data-kind=error]{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}._367UGW_banner[data-kind=info]{color:var(--dsw-alias-state-business-primary);border-color:var(--dsw-alias-state-business-primary)}._367UGW_banner[data-kind=warn]{color:var(--dsw-alias-state-warn-primary);border-color:var(--dsw-alias-state-warn-primary)}._367UGW_card{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:10px;flex-direction:column;flex:none;gap:10px;padding:12px;display:flex}._367UGW_field{flex-direction:column;gap:5px;display:flex}._367UGW_fieldLabel{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600}._367UGW_input{color:var(--dsw-alias-label-primary);background:var(--dsw-specific-input-major);border:1px solid var(--dsw-alias-border-l2);resize:vertical;border-radius:8px;outline:none;padding:7px 10px;font-family:inherit;font-size:13px}._367UGW_input:focus{border-color:var(--dsw-alias-state-business-primary)}._367UGW_input::placeholder{color:var(--dsw-alias-label-tertiary)}._367UGW_input:disabled{opacity:.55}._367UGW_hint{color:var(--dsw-alias-label-tertiary);font-size:11.5px;line-height:1.5}._367UGW_formError{color:var(--dsw-alias-state-error-primary);margin:0;font-size:12px}._367UGW_checkboxRow{color:var(--dsw-alias-label-primary);cursor:pointer;align-items:flex-start;gap:8px;font-size:13px;display:flex}._367UGW_checkboxRow input{flex:none;margin-top:2px}._367UGW_radioLabel{color:var(--dsw-alias-label-primary);cursor:pointer;align-items:center;gap:6px;font-size:13px;display:inline-flex}._367UGW_secretFields{grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px 12px;padding-top:4px;display:grid}._367UGW_hiddenFile{display:none}._367UGW_modeTabs{border-bottom:1px solid var(--dsw-alias-border-l1);flex:none;gap:2px;display:flex}._367UGW_modeTab{color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-bottom:2px solid #0000;padding:6px 12px;font-size:12.5px}._367UGW_modeTab:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}._367UGW_modeTab[data-active]{color:var(--dsw-alias-label-primary);border-bottom-color:var(--dsw-alias-state-business-primary);font-weight:600}._367UGW_modeHint{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.5}._367UGW_groupList{flex-direction:column;gap:10px;display:flex}._367UGW_groupCard{gap:8px}._367UGW_groupHeader{flex-wrap:wrap;align-items:baseline;gap:8px;display:flex}._367UGW_groupLabel{color:var(--dsw-alias-label-primary);font-size:12.5px;font-weight:700}._367UGW_groupNote{color:var(--dsw-alias-label-tertiary);font-size:11.5px}._367UGW_groupItems{flex-direction:column;gap:6px;display:flex}._367UGW_categoryItem{flex-wrap:wrap;align-items:center;gap:8px;display:inline-flex}._367UGW_categoryName{font-weight:500}._367UGW_categoryDesc{color:var(--dsw-alias-label-tertiary);font-size:11.5px}._367UGW_optionsCard{gap:8px}._367UGW_warnList{margin:4px 0 0;padding-left:18px}._367UGW_actionRow{flex-wrap:wrap;align-items:center;gap:10px;display:flex}._367UGW_progressBlock{flex-direction:column;flex:none;gap:6px;display:flex}._367UGW_progressMeta{color:var(--dsw-alias-label-secondary);flex-wrap:wrap;gap:12px;font-size:12px;display:flex}._367UGW_progressLabel{font-weight:600}._367UGW_progressDetail{color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;max-width:320px;overflow:hidden}._367UGW_progressBadge{font-variant-numeric:tabular-nums;white-space:nowrap;border-radius:999px;align-items:center;gap:2px;padding:1px 8px;font-size:11px;line-height:18px;display:inline-flex}._367UGW_progressBadgeSection{background:color-mix(in srgb, var(--dsw-alias-state-business-primary) 14%, transparent);color:var(--dsw-alias-state-business-primary)}._367UGW_progressBadgeCount{background:color-mix(in srgb, var(--dsw-alias-state-info-primary) 14%, transparent);color:var(--dsw-alias-state-info-primary)}._367UGW_progressPercent{font-variant-numeric:tabular-nums;margin-left:auto}._367UGW_progressTrack{background:var(--dsw-alias-interactive-bg-hover);border-radius:999px;height:8px;overflow:hidden}._367UGW_progressBar{background:var(--dsw-alias-state-business-primary);border-radius:999px;height:100%;transition:width .12s linear}._367UGW_progressBarDone{background:var(--dsw-alias-state-success-primary)}._367UGW_progressIndeterminate{width:40%;animation:1s ease-in-out infinite _367UGW_dshCmIndeterminate}@keyframes _367UGW_dshCmIndeterminate{0%{margin-left:-40%}to{margin-left:100%}}._367UGW_reportView{flex-direction:column;gap:10px;display:flex}._367UGW_statRow{flex-wrap:wrap;align-items:center;gap:6px;display:flex}._367UGW_reportText{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);white-space:pre-wrap;word-break:break-all;max-height:260px;color:var(--dsw-alias-label-primary);border-radius:8px;margin:0;padding:10px 12px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11.5px;line-height:1.6;overflow:auto}._367UGW_reportFooter{justify-content:flex-end;gap:8px;display:flex}._367UGW_rollbackBox{border:1px solid var(--dsw-alias-state-warn-primary);color:var(--dsw-alias-state-warn-primary);border-radius:8px;flex-direction:column;gap:6px;padding:10px 12px;display:flex}._367UGW_rollbackBox ._367UGW_reportText{border-color:var(--dsw-alias-border-l2)}._367UGW_errorBanner{border:1px solid var(--dsw-alias-state-error-primary);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);border-radius:8px;flex-direction:column;gap:6px;padding:10px 12px;display:flex}._367UGW_errorTitle{color:var(--dsw-alias-state-error-primary);font-size:13px;font-weight:700}._367UGW_errorReason{white-space:pre-wrap;word-break:break-all;color:var(--dsw-alias-label-secondary);margin:0;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11.5px}._367UGW_errorAction{color:var(--dsw-alias-label-secondary);font-size:12.5px}._367UGW_errorActionLabel{color:var(--dsw-alias-state-business-primary)}._367UGW_errorItem{color:var(--dsw-alias-label-tertiary);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11.5px}._367UGW_errorFooter{justify-content:flex-end;display:flex}._367UGW_errorList{flex-direction:column;gap:4px;display:flex}._367UGW_errorLine{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);white-space:pre-wrap;word-break:break-all;color:var(--dsw-alias-state-error-primary);border-radius:6px;margin:0;padding:6px 10px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px}._367UGW_conflictList{flex-direction:column;gap:10px;display:flex}._367UGW_conflictItem{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;flex-direction:column;gap:6px;padding:10px 12px;display:flex}._367UGW_conflictHead{flex-wrap:wrap;align-items:center;gap:8px;display:flex}._367UGW_conflictId{color:var(--dsw-alias-label-primary);font-size:12.5px;font-weight:600}._367UGW_severityError{color:var(--dsw-alias-state-error-primary);border:1px solid var(--dsw-alias-state-error-primary);border-radius:999px;padding:0 6px;font-size:10.5px}._367UGW_conflictDetail{background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2);white-space:pre-wrap;word-break:break-all;color:var(--dsw-alias-label-secondary);border-radius:6px;margin:0;padding:6px 8px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px}._367UGW_conflictOptions{flex-wrap:wrap;gap:14px;display:flex}._367UGW_pathMappingList{flex-direction:column;gap:8px;display:flex}._367UGW_pathRow{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:10px;padding:10px 12px;display:grid}._367UGW_pathOld,._367UGW_pathNew{flex-direction:column;gap:4px;min-width:0;display:flex}._367UGW_pathValue{text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-primary);margin:0;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11.5px;overflow:hidden}._367UGW_pathIssueKind{color:var(--dsw-alias-state-warn-primary);font-size:10.5px}._367UGW_secretsList{flex-direction:column;gap:10px;display:flex}._367UGW_empty{text-align:center;color:var(--dsw-alias-label-tertiary);padding:22px 12px;font-size:12.5px}._367UGW_spinnerWrap{align-items:center;gap:6px;display:inline-flex}._367UGW_spinner{border:2px solid var(--dsw-alias-state-business-primary);vertical-align:-1px;border-top-color:#0000;border-radius:50%;flex:none;width:11px;height:11px;animation:.8s linear infinite _367UGW_dshCmSpin;display:inline-block}._367UGW_spinnerLabel{color:var(--dsw-alias-label-secondary);font-size:12px}@keyframes _367UGW_dshCmSpin{to{transform:rotate(360deg)}}._367UGW_snapshotList{flex-direction:column;gap:2px;display:flex}._367UGW_snapshotRowHeader,._367UGW_snapshotRow{grid-template-columns:minmax(120px,1.2fr) minmax(140px,1.6fr) 90px 56px 56px;align-items:center;gap:8px;padding:6px 10px;font-size:12px;display:grid}._367UGW_snapshotRowHeader{color:var(--dsw-alias-label-tertiary);border-bottom:1px solid var(--dsw-alias-border-l1);font-size:11px}._367UGW_snapshotRow{text-align:left;width:100%;color:var(--dsw-alias-label-primary);cursor:pointer;background:0 0;border:1px solid #0000;border-radius:6px}._367UGW_snapshotRow:hover{background:var(--dsw-alias-interactive-bg-hover)}._367UGW_snapshotRow[data-active]{border-color:var(--dsw-alias-state-business-primary);background:color-mix(in srgb, var(--dsw-alias-state-business-primary) 10%, transparent)}._367UGW_warnText{color:var(--dsw-alias-state-warn-primary)}._367UGW_reportList{color:var(--dsw-alias-label-primary);flex-direction:column;gap:3px;margin:8px 0 4px;padding-left:18px;font-size:12px;display:flex}._367UGW_kindTag{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary);border-radius:4px;padding:1px 6px;font-size:10.5px;display:inline-block}._367UGW_planScroll{overscroll-behavior:contain;background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;height:280px;padding:2px 10px 8px;overflow-y:auto}._367UGW_planScroll ._367UGW_reportList{margin-top:6px}._367UGW_reportScroll{overscroll-behavior:contain;background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;max-height:220px;padding:2px 10px 6px;overflow-y:auto}._367UGW_reportScroll ._367UGW_reportList{margin-top:4px}._367UGW_rowActions{gap:8px;margin-top:10px;display:flex}._367UGW_diffScroll{overscroll-behavior:contain;background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2);white-space:pre-wrap;word-break:break-all;max-height:240px;color:var(--dsw-alias-label-primary);border-radius:8px;margin:0;padding:8px 10px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11.5px;line-height:1.6;overflow:auto}";
1943
2069
  const tagId = "dsh-config-manager/config-manager.module.css";
1944
2070
  if (typeof document !== "undefined" && document.querySelector("style[data-tag=\"dsh-config-manager/config-manager.module.css\"]") === null) {
1945
2071
  const tag = document.createElement("style");
@@ -1949,104 +2075,105 @@ if (typeof document !== "undefined" && document.querySelector("style[data-tag=\"
1949
2075
  document.head.appendChild(tag);
1950
2076
  }
1951
2077
  var config_manager_module_css_default = {
1952
- "errorBanner": "_367UGW_errorBanner",
2078
+ "categoryItem": "_367UGW_categoryItem",
2079
+ "warnText": "_367UGW_warnText",
2080
+ "rowActions": "_367UGW_rowActions",
2081
+ "groupLabel": "_367UGW_groupLabel",
2082
+ "groupNote": "_367UGW_groupNote",
2083
+ "warnList": "_367UGW_warnList",
2084
+ "secretsList": "_367UGW_secretsList",
2085
+ "progressBadge": "_367UGW_progressBadge",
2086
+ "viewBody": "_367UGW_viewBody",
2087
+ "progressPercent": "_367UGW_progressPercent",
2088
+ "sectionHeader": "_367UGW_sectionHeader",
2089
+ "field": "_367UGW_field",
2090
+ "categoryDesc": "_367UGW_categoryDesc",
2091
+ "pathIssueKind": "_367UGW_pathIssueKind",
2092
+ "progressBarDone": "_367UGW_progressBarDone",
2093
+ "modeTabs": "_367UGW_modeTabs",
2094
+ "reportView": "_367UGW_reportView",
1953
2095
  "snapshotRow": "_367UGW_snapshotRow",
2096
+ "dshCmIndeterminate": "_367UGW_dshCmIndeterminate",
2097
+ "snapshotRowHeader": "_367UGW_snapshotRowHeader",
2098
+ "errorLine": "_367UGW_errorLine",
2099
+ "banner": "_367UGW_banner",
2100
+ "radioLabel": "_367UGW_radioLabel",
2101
+ "progressMeta": "_367UGW_progressMeta",
2102
+ "groupItems": "_367UGW_groupItems",
2103
+ "pathMappingList": "_367UGW_pathMappingList",
2104
+ "sectionTitle": "_367UGW_sectionTitle",
1954
2105
  "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",
2106
+ "reportList": "_367UGW_reportList",
2107
+ "dangerButton": "_367UGW_dangerButton",
1961
2108
  "errorReason": "_367UGW_errorReason",
2109
+ "viewTab": "_367UGW_viewTab",
2110
+ "conflictItem": "_367UGW_conflictItem",
2111
+ "diffScroll": "_367UGW_diffScroll",
2112
+ "progressBadgeCount": "_367UGW_progressBadgeCount",
1962
2113
  "formError": "_367UGW_formError",
1963
- "pathOld": "_367UGW_pathOld",
1964
- "progressBlock": "_367UGW_progressBlock",
1965
2114
  "card": "_367UGW_card",
1966
- "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",
1973
- "progressBadgeSection": "_367UGW_progressBadgeSection",
1974
- "warnList": "_367UGW_warnList",
1975
- "actionRow": "_367UGW_actionRow",
1976
- "severityError": "_367UGW_severityError",
2115
+ "primaryButton": "_367UGW_primaryButton",
2116
+ "conflictOptions": "_367UGW_conflictOptions",
2117
+ "pathRow": "_367UGW_pathRow",
2118
+ "sectionBody": "_367UGW_sectionBody",
2119
+ "secretFields": "_367UGW_secretFields",
2120
+ "optionsCard": "_367UGW_optionsCard",
2121
+ "statRow": "_367UGW_statRow",
2122
+ "rollbackBox": "_367UGW_rollbackBox",
2123
+ "badgeOk": "_367UGW_badgeOk",
2124
+ "progressBar": "_367UGW_progressBar",
1977
2125
  "progressLabel": "_367UGW_progressLabel",
1978
- "section": "_367UGW_section",
1979
2126
  "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",
2127
+ "errorAction": "_367UGW_errorAction",
1986
2128
  "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",
2129
+ "errorItem": "_367UGW_errorItem",
2130
+ "conflictHead": "_367UGW_conflictHead",
2131
+ "severityError": "_367UGW_severityError",
1993
2132
  "fieldLabel": "_367UGW_fieldLabel",
1994
- "progressTrack": "_367UGW_progressTrack",
1995
- "reportScroll": "_367UGW_reportScroll",
1996
- "pathRow": "_367UGW_pathRow",
1997
- "modeTab": "_367UGW_modeTab",
1998
- "hint": "_367UGW_hint",
1999
- "secretFields": "_367UGW_secretFields",
2000
- "reportView": "_367UGW_reportView",
2001
- "errorFooter": "_367UGW_errorFooter",
2002
- "pathMappingList": "_367UGW_pathMappingList",
2003
- "pathValue": "_367UGW_pathValue",
2133
+ "actionRow": "_367UGW_actionRow",
2134
+ "errorBanner": "_367UGW_errorBanner",
2135
+ "empty": "_367UGW_empty",
2004
2136
  "spinner": "_367UGW_spinner",
2137
+ "kindTag": "_367UGW_kindTag",
2138
+ "spinnerLabel": "_367UGW_spinnerLabel",
2005
2139
  "input": "_367UGW_input",
2006
- "progressIndeterminate": "_367UGW_progressIndeterminate",
2007
- "groupList": "_367UGW_groupList",
2140
+ "errorActionLabel": "_367UGW_errorActionLabel",
2141
+ "planScroll": "_367UGW_planScroll",
2142
+ "errorList": "_367UGW_errorList",
2008
2143
  "progressDetail": "_367UGW_progressDetail",
2009
- "optionsCard": "_367UGW_optionsCard",
2144
+ "snapshotList": "_367UGW_snapshotList",
2010
2145
  "ghostButton": "_367UGW_ghostButton",
2011
- "errorLine": "_367UGW_errorLine",
2012
- "modeTabs": "_367UGW_modeTabs",
2013
- "progressBadgeCount": "_367UGW_progressBadgeCount",
2014
- "progressMeta": "_367UGW_progressMeta",
2015
- "errorTitle": "_367UGW_errorTitle",
2016
- "groupItems": "_367UGW_groupItems",
2017
- "sectionHeader": "_367UGW_sectionHeader",
2018
- "errorAction": "_367UGW_errorAction",
2019
- "errorActionLabel": "_367UGW_errorActionLabel",
2020
- "dangerButton": "_367UGW_dangerButton",
2021
- "errorItem": "_367UGW_errorItem",
2022
- "secretsList": "_367UGW_secretsList",
2023
- "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",
2146
+ "hint": "_367UGW_hint",
2147
+ "progressIndeterminate": "_367UGW_progressIndeterminate",
2148
+ "conflictDetail": "_367UGW_conflictDetail",
2149
+ "pathValue": "_367UGW_pathValue",
2030
2150
  "badgeError": "_367UGW_badgeError",
2031
- "banner": "_367UGW_banner",
2032
- "modeHint": "_367UGW_modeHint",
2151
+ "sectionSubtitle": "_367UGW_sectionSubtitle",
2152
+ "viewTabs": "_367UGW_viewTabs",
2153
+ "modeTab": "_367UGW_modeTab",
2154
+ "categoryName": "_367UGW_categoryName",
2155
+ "progressTrack": "_367UGW_progressTrack",
2156
+ "checkboxRow": "_367UGW_checkboxRow",
2157
+ "reportText": "_367UGW_reportText",
2158
+ "errorFooter": "_367UGW_errorFooter",
2159
+ "hiddenFile": "_367UGW_hiddenFile",
2160
+ "conflictList": "_367UGW_conflictList",
2161
+ "errorTitle": "_367UGW_errorTitle",
2162
+ "conflictId": "_367UGW_conflictId",
2163
+ "reportFooter": "_367UGW_reportFooter",
2164
+ "pathOld": "_367UGW_pathOld",
2165
+ "badgeWarn": "_367UGW_badgeWarn",
2166
+ "section": "_367UGW_section",
2167
+ "reportScroll": "_367UGW_reportScroll",
2033
2168
  "sectionTitleBlock": "_367UGW_sectionTitleBlock",
2034
- "primaryButton": "_367UGW_primaryButton",
2035
- "categoryItem": "_367UGW_categoryItem",
2036
- "progressBar": "_367UGW_progressBar",
2037
- "conflictHead": "_367UGW_conflictHead",
2038
- "conflictDetail": "_367UGW_conflictDetail",
2039
- "conflictOptions": "_367UGW_conflictOptions",
2040
- "spinnerWrap": "_367UGW_spinnerWrap",
2041
- "warnText": "_367UGW_warnText",
2042
- "planScroll": "_367UGW_planScroll",
2043
- "dshCmIndeterminate": "_367UGW_dshCmIndeterminate",
2044
- "sectionBody": "_367UGW_sectionBody",
2169
+ "modeHint": "_367UGW_modeHint",
2170
+ "progressBadgeSection": "_367UGW_progressBadgeSection",
2171
+ "groupList": "_367UGW_groupList",
2045
2172
  "groupCard": "_367UGW_groupCard",
2046
- "reportFooter": "_367UGW_reportFooter",
2047
- "rollbackBox": "_367UGW_rollbackBox",
2048
- "errorList": "_367UGW_errorList",
2049
- "badgeOk": "_367UGW_badgeOk"
2173
+ "groupHeader": "_367UGW_groupHeader",
2174
+ "spinnerWrap": "_367UGW_spinnerWrap",
2175
+ "dshCmSpin": "_367UGW_dshCmSpin",
2176
+ "progressBlock": "_367UGW_progressBlock"
2050
2177
  };
2051
2178
  //#endregion
2052
2179
  //#region src/client/common/ui.tsx
@@ -3802,7 +3929,14 @@ const SYNC_API = {
3802
3929
  pull: "/api/dsh-config-manager/sync/pull",
3803
3930
  githubStart: "/api/dsh-config-manager/sync/github/start",
3804
3931
  githubPoll: "/api/dsh-config-manager/sync/github/poll",
3805
- githubCancel: "/api/dsh-config-manager/sync/github/cancel"
3932
+ githubCancel: "/api/dsh-config-manager/sync/github/cancel",
3933
+ history: "/api/dsh-config-manager/sync/history",
3934
+ snapshotsList: "/api/dsh-config-manager/sync/snapshots-list",
3935
+ sync: "/api/dsh-config-manager/sync/sync",
3936
+ applyItems: "/api/dsh-config-manager/sync/apply-items",
3937
+ cancel: "/api/dsh-config-manager/sync/cancel",
3938
+ autosync: "/api/dsh-config-manager/sync/autosync",
3939
+ rollback: "/api/dsh-config-manager/sync/rollback"
3806
3940
  };
3807
3941
  /** DSH credentials 中的同步 token 引用名(Host 半同值;仅供提示文案使用,值由 Host 读写) */
3808
3942
  const SYNC_CREDENTIAL_REF = "DSH_CONFIG_MANAGER_SYNC_TOKEN";
@@ -3869,6 +4003,38 @@ var SyncApi = class {
3869
4003
  async githubCancel(flowId) {
3870
4004
  return postJson(SYNC_API.githubCancel, { flowId }, this.t);
3871
4005
  }
4006
+ /** 同步历史:列出本地祖先快照 + 自动同步执行记录(按 createdAt 倒序合并)。 */
4007
+ async history() {
4008
+ return readJson(await fetch(SYNC_API.history), this.t);
4009
+ }
4010
+ /** 远端历史快照列表(供「选择历史快照」下拉)。 */
4011
+ async snapshotsList(payload) {
4012
+ return postJson(SYNC_API.snapshotsList, payload, this.t);
4013
+ }
4014
+ /** 一键同步第一步:拉取 → 差异确认会话(items 逐项确认,暂不导入)。 */
4015
+ async sync(payload) {
4016
+ return postJson(SYNC_API.sync, payload, this.t);
4017
+ }
4018
+ /** 一键同步第二步:按用户对差异项的逐项决策执行导入。 */
4019
+ async applyItems(payload) {
4020
+ return postJson(SYNC_API.applyItems, payload, this.t);
4021
+ }
4022
+ /** 取消/清理差异确认会话(丢弃临时 ZIP,零副作用)。 */
4023
+ async cancel(syncSessionId) {
4024
+ return postJson(SYNC_API.cancel, { syncSessionId }, this.t);
4025
+ }
4026
+ /** 自动同步状态(GET /sync/autosync)。 */
4027
+ async autosyncStatus() {
4028
+ return readJson(await fetch(SYNC_API.autosync), this.t);
4029
+ }
4030
+ /** 自动同步配置更新(POST /sync/autosync)。 */
4031
+ async autosyncUpdate(payload) {
4032
+ return postJson(SYNC_API.autosync, payload, this.t);
4033
+ }
4034
+ /** 一键回滚:按 restoreId 调用 backup→rollback */
4035
+ async rollback(payload) {
4036
+ return postJson(SYNC_API.rollback, payload, this.t);
4037
+ }
3872
4038
  };
3873
4039
  //#endregion
3874
4040
  //#region src/client/sync/sync-view.ts
@@ -3946,8 +4112,8 @@ function computeSyncButtons(busy, repoUrl, t = zhUiT) {
3946
4112
  return {
3947
4113
  canPush: enabled,
3948
4114
  canPull: enabled,
3949
- pushLabel: busy === "push" ? t("sync.pushing") : t("sync.pushLabel"),
3950
- pullLabel: busy === "pull" ? t("sync.pulling") : t("sync.pullLabel")
4115
+ pushLabel: busy === "push" ? t("sync.pushing") : busy === "sync" ? t("sync.syncing") : t("sync.pushLabel"),
4116
+ pullLabel: busy === "pull" ? t("sync.pulling") : busy === "sync" ? t("sync.syncing") : t("sync.pullLabel")
3951
4117
  };
3952
4118
  }
3953
4119
  /** 状态行渲染模型:加载 / 未配置 / 就绪(凭据 + 上次同步 + 通道)/ 错误 */
@@ -3966,11 +4132,11 @@ function computeSyncStatus(statusInfo, loading, error, t = zhUiT) {
3966
4132
  };
3967
4133
  return {
3968
4134
  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}` : ""}` : ""}`
4135
+ 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
4136
  };
3971
4137
  }
3972
4138
  /** ISO-8601 → 本地可读时间(YYYY-MM-DD HH:mm;非法输入原样返回) */
3973
- function formatDateTime(iso) {
4139
+ function formatDateTime$1(iso) {
3974
4140
  const d = new Date(iso);
3975
4141
  if (Number.isNaN(d.getTime())) return iso;
3976
4142
  const pad = (n) => String(n).padStart(2, "0");
@@ -4065,6 +4231,586 @@ function githubPollMessage(poll, t = zhUiT) {
4065
4231
  default: return "";
4066
4232
  }
4067
4233
  }
4234
+ /** 需要人工决策的 PlanItemKind(与 Host /sync/sync 的 needsReview 判定对齐)。 */
4235
+ const CONFIRM_REVIEW_KINDS = /* @__PURE__ */ new Set([
4236
+ "Conflict",
4237
+ "MissingSecret",
4238
+ "MissingDependency",
4239
+ "Install",
4240
+ "Error",
4241
+ "PathMapping"
4242
+ ]);
4243
+ /** 差异确认列表摘要(按 severity 计数 + 采用数 + needsReview 徽章数据源)。 */
4244
+ function summarizeConfirmItems(items) {
4245
+ let info = 0;
4246
+ let warning = 0;
4247
+ let error = 0;
4248
+ let adopted = 0;
4249
+ let needsReview = false;
4250
+ for (const it of items) {
4251
+ if (it.severity === "error") error += 1;
4252
+ else if (it.severity === "warning") warning += 1;
4253
+ else info += 1;
4254
+ if (it.adopt) adopted += 1;
4255
+ if (CONFIRM_REVIEW_KINDS.has(it.kind)) needsReview = true;
4256
+ }
4257
+ return {
4258
+ total: items.length,
4259
+ info,
4260
+ warning,
4261
+ error,
4262
+ adopted,
4263
+ needsReview
4264
+ };
4265
+ }
4266
+ /** AutosyncInterval → ms。 */
4267
+ function autosyncIntervalMs(interval) {
4268
+ switch (interval) {
4269
+ case "5m": return 300 * 1e3;
4270
+ case "15m": return 900 * 1e3;
4271
+ case "60m": return 3600 * 1e3;
4272
+ case "6h": return 360 * 60 * 1e3;
4273
+ case "12h": return 720 * 60 * 1e3;
4274
+ case "24h": return 1440 * 60 * 1e3;
4275
+ default: return 1800 * 1e3;
4276
+ }
4277
+ }
4278
+ /** 距下次自动同步剩余 ms(已到期 → 0)。elapsedMs 为 host 计算的「距上次执行已过 ms」。 */
4279
+ function computeAutosyncCountdown(elapsedMs, intervalMs) {
4280
+ if (elapsedMs < 0) return -1;
4281
+ return Math.max(0, intervalMs - elapsedMs);
4282
+ }
4283
+ /** 间隔 ms → 可读时长(如「30 分钟」「6 小时」)。 */
4284
+ function formatIntervalDuration(ms, t = zhUiT) {
4285
+ const minutes = Math.round(ms / 6e4);
4286
+ if (minutes < 60) return t("sync.interval.30m");
4287
+ const hours = Math.round(minutes / 60);
4288
+ if (hours === 6) return t("sync.interval.6h");
4289
+ if (hours === 12) return t("sync.interval.12h");
4290
+ if (hours === 24) return t("sync.interval.24h");
4291
+ return `${hours}h`;
4292
+ }
4293
+ /** 自动同步状态行的可读文案(未运行 / 上次状态 / 连续失败计数)。 */
4294
+ function autosyncStatusText(status, t = zhUiT) {
4295
+ if (status.lastRunAt === void 0 || status.lastRunAt === "" || status.lastRunStatus === void 0) return t("sync.autosyncNever");
4296
+ return `${status.lastRunStatus === "success" ? t("sync.autosyncSuccess") : status.lastRunStatus === "skipped" ? t("sync.autosyncSkipped") : status.lastRunStatus === "partial" ? t("sync.autosyncPartial") : t("sync.autosyncFailed")} · ${t("sync.autosyncLastRun", { time: formatDateTime$1(status.lastRunAt) })}${status.consecutiveFailures > 0 ? ` · ${t("sync.autosyncFailCount", { n: String(status.consecutiveFailures) })}` : ""}`;
4297
+ }
4298
+ //#endregion
4299
+ //#region src/client/sync/history-model.ts
4300
+ /** 统一历史条目(快照 + 自动同步)按 createdAt 倒序排序。 */
4301
+ function projectSyncHistoryEntries(entries) {
4302
+ return [...entries].sort(byCreatedAtDesc);
4303
+ }
4304
+ function byCreatedAtDesc(a, b) {
4305
+ return a.createdAt < b.createdAt ? 1 : a.createdAt > b.createdAt ? -1 : 0;
4306
+ }
4307
+ /** ISO 时间 → 本地可读字符串(短格式) */
4308
+ function formatDateTime(iso) {
4309
+ if (!iso) return "—";
4310
+ const d = new Date(iso);
4311
+ if (isNaN(d.getTime())) return iso;
4312
+ const pad = (n) => String(n).padStart(2, "0");
4313
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
4314
+ }
4315
+ /** 自动同步执行记录的方向可读标签。 */
4316
+ function directionLabel(direction) {
4317
+ switch (direction) {
4318
+ case "pull": return "下载";
4319
+ case "push": return "上传";
4320
+ default: return "双向";
4321
+ }
4322
+ }
4323
+ /** 自动同步执行状态可读标签。 */
4324
+ function autosyncStatusLabel(status) {
4325
+ switch (status) {
4326
+ case "success": return "成功";
4327
+ case "skipped": return "已跳过";
4328
+ case "partial": return "部分成功";
4329
+ default: return "失败";
4330
+ }
4331
+ }
4332
+ /** 跳过原因 → 可读描述(host 透传语义;未知原因回退原串)。 */
4333
+ function describeSkipReason(reason) {
4334
+ switch (reason) {
4335
+ case "conflict": return "冲突项被跳过";
4336
+ case "no-remote": return "远端无快照";
4337
+ case "not-configured": return "未配置仓库";
4338
+ case "network": return "网络问题";
4339
+ default: return reason ?? "未知";
4340
+ }
4341
+ }
4342
+ /** 自动同步记录 → 展示行投影。 */
4343
+ function projectAutosyncEntry(entry) {
4344
+ const parts = [directionLabel(entry.direction), autosyncStatusLabel(entry.status)];
4345
+ if (entry.skipReason !== void 0) parts.push(describeSkipReason(entry.skipReason));
4346
+ return {
4347
+ id: entry.createdAt,
4348
+ createdAt: entry.createdAt,
4349
+ direction: directionLabel(entry.direction),
4350
+ status: autosyncStatusLabel(entry.status),
4351
+ summary: parts.join(" · "),
4352
+ conflictedSections: entry.conflictedSections,
4353
+ appliedSections: entry.appliedSections,
4354
+ error: entry.error,
4355
+ notifiedAt: entry.notifiedAt,
4356
+ hasDetail: entry.conflictedSections !== void 0 && entry.conflictedSections.length > 0 || entry.appliedSections !== void 0 && entry.appliedSections.length > 0 || entry.error !== void 0
4357
+ };
4358
+ }
4359
+ //#endregion
4360
+ //#region src/client/sync/SyncHistoryView.tsx
4361
+ /**
4362
+ * 同步历史视图(方案 A):列出本地祖先快照目录(kind=apply)+ 自动同步执行记录
4363
+ * (kind=autosync)。自动同步行显示时间/方向/状态/跳过冲突/应用分区,点开可看被跳过
4364
+ * 冲突分区明细。
4365
+ *
4366
+ * 数据获取:GET /sync/history → { entries: SyncHistoryEntry[] }(按 createdAt 倒序合并)。
4367
+ * 纯函数投影在 ./history-model.ts(node --test 可测),本组件只做装配。
4368
+ */
4369
+ function SyncHistoryView(props) {
4370
+ const { api, t } = props;
4371
+ const [loading, setLoading] = (0, react.useState)(true);
4372
+ const [error, setError] = (0, react.useState)(null);
4373
+ const [entries, setEntries] = (0, react.useState)([]);
4374
+ (0, react.useEffect)(() => {
4375
+ let cancelled = false;
4376
+ (async () => {
4377
+ try {
4378
+ const data = await api.history();
4379
+ if (!cancelled) {
4380
+ setEntries(data.entries);
4381
+ setLoading(false);
4382
+ }
4383
+ } catch (err) {
4384
+ if (!cancelled) {
4385
+ setError(err instanceof Error ? err.message : String(err));
4386
+ setLoading(false);
4387
+ }
4388
+ }
4389
+ })();
4390
+ return () => {
4391
+ cancelled = true;
4392
+ };
4393
+ }, [api]);
4394
+ const rows = (0, react.useMemo)(() => projectSyncHistoryEntries(entries), [entries]);
4395
+ const snapshotRows = (0, react.useMemo)(() => rows.filter((r) => r.kind === "apply" || r.kind === "push" || r.kind === "pull" || r.kind === "rollback").map((r) => ({
4396
+ id: r.id,
4397
+ createdAt: r.createdAt,
4398
+ sectionCount: r.sectionCount ?? 0,
4399
+ reviewCount: r.reviewCount ?? 0
4400
+ })), [rows]);
4401
+ if (loading) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Spinner, { label: t("common.loading") });
4402
+ if (error) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Card, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4403
+ className: "error",
4404
+ children: error
4405
+ }) });
4406
+ if (rows.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Card, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t("history.empty") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("history.emptyHint") })] });
4407
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Card, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SectionTitle, { title: `${t("history.title")}(${rows.length})` }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
4408
+ className: "sync-history-table",
4409
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
4410
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("history.colTime") }),
4411
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("history.colKind") }),
4412
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("history.colDetail") })
4413
+ ] }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: rows.map((r) => {
4414
+ if (r.kind === "autosync" && r.autosync !== void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AutosyncRow, {
4415
+ entry: r.autosync,
4416
+ t
4417
+ }, r.id);
4418
+ const snap = snapshotRows.find((s) => s.id === r.id);
4419
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
4420
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: formatDateTime(r.createdAt) }),
4421
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
4422
+ kind: "info",
4423
+ children: t("history.kindSnapshot")
4424
+ }) }),
4425
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("td", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: r.id }), snap !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
4426
+ " · ",
4427
+ snap.sectionCount,
4428
+ " ",
4429
+ t("history.sectionCount")
4430
+ ] })] })
4431
+ ] }, r.id);
4432
+ }) })]
4433
+ })] });
4434
+ }
4435
+ function AutosyncRow({ entry, t }) {
4436
+ const row = projectAutosyncEntry(entry);
4437
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
4438
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: formatDateTime(row.createdAt) }),
4439
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
4440
+ kind: "info",
4441
+ children: t("history.kindAutosync")
4442
+ }) }),
4443
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("td", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
4444
+ row.summary,
4445
+ entry.pushedSnapshotId !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
4446
+ " · ",
4447
+ t("history.autosyncPush"),
4448
+ " ",
4449
+ entry.pushedSnapshotId
4450
+ ] }),
4451
+ entry.pulledSnapshotId !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
4452
+ " · ",
4453
+ t("history.autosyncPull"),
4454
+ " ",
4455
+ entry.pulledSnapshotId
4456
+ ] })
4457
+ ] }), row.hasDetail && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", { children: t("history.detail") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4458
+ className: config_manager_module_css_default.reportList,
4459
+ children: [
4460
+ row.conflictedSections !== void 0 && row.conflictedSections.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4461
+ className: config_manager_module_css_default.fieldLabel,
4462
+ children: t("history.autosyncConflicted", { sections: "" })
4463
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4464
+ className: config_manager_module_css_default.statRow,
4465
+ children: row.conflictedSections.map((sid) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
4466
+ kind: "warn",
4467
+ children: sid
4468
+ }, sid))
4469
+ })] }),
4470
+ row.appliedSections !== void 0 && row.appliedSections.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4471
+ className: config_manager_module_css_default.fieldLabel,
4472
+ children: t("history.autosyncApplied", { sections: "" })
4473
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4474
+ className: config_manager_module_css_default.statRow,
4475
+ children: row.appliedSections.map((sid) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
4476
+ kind: "ok",
4477
+ children: sid
4478
+ }, sid))
4479
+ })] }),
4480
+ row.error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4481
+ className: config_manager_module_css_default.fieldLabel,
4482
+ children: t("history.autosyncError", { error: "" })
4483
+ }), row.error] })
4484
+ ]
4485
+ })] })] })
4486
+ ] });
4487
+ }
4488
+ //#endregion
4489
+ //#region src/client/sync/SyncConfirmView.tsx
4490
+ /**
4491
+ * 一键同步差异确认视图(方案 A §5 差异确认数据流)。
4492
+ *
4493
+ * 消费 POST /sync/sync 返回的 items[],渲染逐项确认列表:
4494
+ * - 每项:kindTag + description + severity + 「采用远端」复选框(默认勾选,可取消);
4495
+ * - Conflict 项:内联弹窗(用本地 / 用远端 / 跳过);
4496
+ * - 底部:「确认导入」(apply-items)+「取消」(cancel)。
4497
+ *
4498
+ * 全部渲染模型来自 ./sync-view.ts 纯函数(node 单测覆盖),组件只做装配 + 交互状态。
4499
+ */
4500
+ function SyncConfirmView(props) {
4501
+ const { api, syncSessionId, snapshotId, items, needsReview, compatibility, t, onCancel, onRollbackDone } = props;
4502
+ const [states, setStates] = (0, react.useState)(() => {
4503
+ const init = {};
4504
+ for (const it of items) init[it.itemId] = {
4505
+ adopted: it.defaultAdopt,
4506
+ resolution: void 0
4507
+ };
4508
+ return init;
4509
+ });
4510
+ const [phase, setPhase] = (0, react.useState)("idle");
4511
+ const [applyResult, setApplyResult] = (0, react.useState)(null);
4512
+ const [error, setError] = (0, react.useState)(null);
4513
+ const summary = (0, react.useMemo)(() => summarizeConfirmItems(items.map((it) => ({
4514
+ ...it,
4515
+ adopt: states[it.itemId]?.adopted ?? it.defaultAdopt
4516
+ }))), [items, states]);
4517
+ const setAdopted = (itemId, adopted) => {
4518
+ setStates((prev) => ({
4519
+ ...prev,
4520
+ [itemId]: {
4521
+ ...prev[itemId],
4522
+ adopted
4523
+ }
4524
+ }));
4525
+ };
4526
+ const setResolution = (itemId, resolution) => {
4527
+ setStates((prev) => {
4528
+ const existing = prev[itemId] ?? {
4529
+ adopted: false,
4530
+ resolution: void 0
4531
+ };
4532
+ return {
4533
+ ...prev,
4534
+ [itemId]: {
4535
+ ...existing,
4536
+ resolution
4537
+ }
4538
+ };
4539
+ });
4540
+ };
4541
+ const runApply = async () => {
4542
+ const unresolved = items.find((it) => it.kind === "Conflict" && states[it.itemId]?.adopted === true && states[it.itemId]?.resolution === void 0);
4543
+ if (unresolved !== void 0) {
4544
+ setError(t("syncflow.conflictUnresolved", { itemId: unresolved.itemId }));
4545
+ return;
4546
+ }
4547
+ setPhase("applying");
4548
+ setError(null);
4549
+ try {
4550
+ const adoptedMap = /* @__PURE__ */ new Map();
4551
+ for (const it of items) adoptedMap.set(it.itemId, states[it.itemId]?.adopted ?? false);
4552
+ const adoptions = items.filter((it) => adoptedMap.get(it.itemId) === true).map((it) => {
4553
+ const adoption = {
4554
+ itemId: it.itemId,
4555
+ adopt: true
4556
+ };
4557
+ const res = states[it.itemId]?.resolution;
4558
+ if (it.kind === "Conflict" && res !== void 0 && res !== "skip") adoption.resolution = res;
4559
+ return adoption;
4560
+ });
4561
+ const result = await api.applyItems({
4562
+ syncSessionId,
4563
+ adoptions
4564
+ });
4565
+ setPhase(result.ok ? "done" : "failed");
4566
+ setApplyResult(result);
4567
+ } catch (err) {
4568
+ setPhase("failed");
4569
+ setError(err instanceof Error ? err.message : String(err));
4570
+ }
4571
+ };
4572
+ const runCancel = async () => {
4573
+ setPhase("cancelling");
4574
+ setError(null);
4575
+ try {
4576
+ await api.cancel(syncSessionId);
4577
+ } catch {}
4578
+ onCancel();
4579
+ };
4580
+ const runRollback = async (restoreId) => {
4581
+ setPhase("applying");
4582
+ setError(null);
4583
+ try {
4584
+ await api.rollback({ restoreId });
4585
+ setPhase("done");
4586
+ setApplyResult(null);
4587
+ onRollbackDone?.();
4588
+ } catch (err) {
4589
+ setPhase("failed");
4590
+ setError(err instanceof Error ? err.message : String(err));
4591
+ }
4592
+ };
4593
+ const busy = phase === "applying" || phase === "cancelling";
4594
+ if (items.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Card, { children: [
4595
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4596
+ className: config_manager_module_css_default.groupLabel,
4597
+ children: t("syncflow.title")
4598
+ }),
4599
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Banner, {
4600
+ kind: "ok",
4601
+ children: t("syncflow.empty")
4602
+ }),
4603
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4604
+ className: config_manager_module_css_default.actionRow,
4605
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
4606
+ disabled: busy,
4607
+ onClick: () => {
4608
+ runCancel();
4609
+ },
4610
+ children: t("syncflow.cancel")
4611
+ })
4612
+ })
4613
+ ] });
4614
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Card, { children: [
4615
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4616
+ className: config_manager_module_css_default.groupLabel,
4617
+ children: [t("syncflow.title"), compatibility !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
4618
+ kind: "info",
4619
+ children: compatibility
4620
+ })]
4621
+ }),
4622
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Banner, {
4623
+ kind: needsReview ? "warn" : "info",
4624
+ children: needsReview ? t("syncflow.needsReviewBadge") : t("syncflow.diffCount", { count: String(summary.total) })
4625
+ }),
4626
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4627
+ className: config_manager_module_css_default.statRow,
4628
+ children: [
4629
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
4630
+ kind: "info",
4631
+ children: t("syncflow.diffCount", { count: String(summary.total) })
4632
+ }),
4633
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Badge, {
4634
+ kind: "ok",
4635
+ children: [
4636
+ t("syncflow.adoptRemote"),
4637
+ " ",
4638
+ summary.adopted
4639
+ ]
4640
+ }),
4641
+ summary.error > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Badge, {
4642
+ kind: "error",
4643
+ children: [
4644
+ severityLabel("error", api.t),
4645
+ " × ",
4646
+ summary.error
4647
+ ]
4648
+ }),
4649
+ summary.warning > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Badge, {
4650
+ kind: "warn",
4651
+ children: [
4652
+ severityLabel("warning", api.t),
4653
+ " × ",
4654
+ summary.warning
4655
+ ]
4656
+ })
4657
+ ]
4658
+ }),
4659
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4660
+ className: config_manager_module_css_default.hint,
4661
+ children: t("syncflow.adoptHint")
4662
+ }),
4663
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4664
+ className: config_manager_module_css_default.reportList,
4665
+ children: items.map((it) => {
4666
+ const st = states[it.itemId] ?? { adopted: it.defaultAdopt };
4667
+ const isConflict = it.kind === "Conflict";
4668
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4669
+ className: config_manager_module_css_default.statRow,
4670
+ children: [
4671
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
4672
+ className: config_manager_module_css_default.checkboxRow,
4673
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4674
+ type: "checkbox",
4675
+ checked: st.adopted,
4676
+ disabled: busy,
4677
+ onChange: (e) => {
4678
+ setAdopted(it.itemId, e.target.checked);
4679
+ }
4680
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: kindLabel(it.kind, api.t) })]
4681
+ }),
4682
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
4683
+ kind: it.severity === "error" ? "error" : it.severity === "warning" ? "warn" : "info",
4684
+ children: severityLabel(it.severity, api.t)
4685
+ }),
4686
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: it.description }),
4687
+ isConflict && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConflictResolver, {
4688
+ item: it,
4689
+ resolution: st.resolution,
4690
+ busy,
4691
+ t,
4692
+ onResolve: (r) => {
4693
+ setResolution(it.itemId, r);
4694
+ }
4695
+ })
4696
+ ]
4697
+ }, it.itemId);
4698
+ })
4699
+ }),
4700
+ error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Banner, {
4701
+ kind: "error",
4702
+ children: error
4703
+ }),
4704
+ phase === "idle" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4705
+ className: config_manager_module_css_default.actionRow,
4706
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
4707
+ variant: "primary",
4708
+ disabled: busy || summary.adopted === 0,
4709
+ onClick: () => {
4710
+ runApply();
4711
+ },
4712
+ children: t("syncflow.confirmImport")
4713
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
4714
+ disabled: busy,
4715
+ onClick: () => {
4716
+ runCancel();
4717
+ },
4718
+ children: t("syncflow.cancel")
4719
+ })]
4720
+ }),
4721
+ (phase === "done" || phase === "failed") && applyResult !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ApplyResultCard, {
4722
+ result: applyResult,
4723
+ busy,
4724
+ t,
4725
+ onRollback: runRollback
4726
+ }),
4727
+ (phase === "done" || phase === "failed") && applyResult === null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Banner, {
4728
+ kind: "ok",
4729
+ children: t("syncflow.rollbackDone")
4730
+ })
4731
+ ] });
4732
+ }
4733
+ function ConflictResolver({ item, resolution, busy, t, onResolve }) {
4734
+ const conflict = item.conflict;
4735
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4736
+ className: config_manager_module_css_default.rowActions,
4737
+ children: [
4738
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4739
+ className: config_manager_module_css_default.fieldLabel,
4740
+ children: t("syncflow.conflictTitle")
4741
+ }),
4742
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
4743
+ variant: "ghost",
4744
+ disabled: busy || resolution === "keepLocal",
4745
+ onClick: () => {
4746
+ onResolve("keepLocal");
4747
+ },
4748
+ children: resolution === "keepLocal" ? `✓ ${t("syncflow.conflictUseLocal")}` : t("syncflow.conflictUseLocal")
4749
+ }),
4750
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
4751
+ variant: "primary",
4752
+ disabled: busy || resolution === "useRemote",
4753
+ onClick: () => {
4754
+ onResolve("useRemote");
4755
+ },
4756
+ children: resolution === "useRemote" ? `✓ ${t("syncflow.conflictUseRemote")}` : t("syncflow.conflictUseRemote")
4757
+ }),
4758
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
4759
+ variant: "ghost",
4760
+ disabled: busy || resolution === "skip",
4761
+ onClick: () => {
4762
+ onResolve("skip");
4763
+ },
4764
+ children: resolution === "skip" ? `✓ ${t("syncflow.conflictSkip")}` : t("syncflow.conflictSkip")
4765
+ }),
4766
+ conflict?.diff !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
4767
+ className: config_manager_module_css_default.conflictDetail,
4768
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", { children: t("syncflow.diff") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
4769
+ className: config_manager_module_css_default.diffScroll,
4770
+ children: conflict.diff
4771
+ })]
4772
+ })
4773
+ ]
4774
+ });
4775
+ }
4776
+ function ApplyResultCard({ result, busy, t, onRollback }) {
4777
+ const ok = result.ok;
4778
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
4779
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Banner, {
4780
+ kind: ok ? "ok" : "error",
4781
+ children: ok ? t("syncflow.importDone", { n: String(result.applied.length) }) : t("syncflow.importFailed")
4782
+ }),
4783
+ result.needsRestart && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Banner, {
4784
+ kind: "warn",
4785
+ children: t("syncflow.needsRestart")
4786
+ }),
4787
+ result.applied.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4788
+ className: config_manager_module_css_default.fieldLabel,
4789
+ children: t("syncflow.importedSections")
4790
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4791
+ className: config_manager_module_css_default.statRow,
4792
+ children: result.applied.map((sid) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
4793
+ kind: "ok",
4794
+ children: sid
4795
+ }, sid))
4796
+ })] }),
4797
+ result.warnings.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4798
+ className: config_manager_module_css_default.fieldLabel,
4799
+ children: t("syncflow.importWarnings")
4800
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
4801
+ className: config_manager_module_css_default.warnList,
4802
+ children: result.warnings.map((w, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: w }, i))
4803
+ })] }),
4804
+ result.restoreId !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
4805
+ variant: "danger",
4806
+ disabled: busy,
4807
+ onClick: () => {
4808
+ onRollback(result.restoreId);
4809
+ },
4810
+ children: busy ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Spinner, { label: t("syncflow.rollingBack") }) : t("syncflow.rollback")
4811
+ })
4812
+ ] });
4813
+ }
4068
4814
  //#endregion
4069
4815
  //#region src/client/sync/SyncSettingsView.tsx
4070
4816
  /**
@@ -4076,8 +4822,10 @@ function githubPollMessage(poll, t = zhUiT) {
4076
4822
  * + gitBin(可选);
4077
4823
  * - 私有仓库强制提示横幅(常驻);
4078
4824
  * - 推送按钮 → SyncPushReport(快照 id / 分区 / 告警);
4079
- * - 拉取按钮 → SyncPullReport.changes 差异摘要(description/kind/severity)+
4080
- * 「预览不执行导入」提示;needsReview 高亮(v1 不做完整导入接线);
4825
+ * - 拉取按钮 → SyncPullReport.changes 差异摘要(description/kind/severity);
4826
+ * - 【方案 A】一键同步主按钮:拉取 → 差异确认会话(SyncConfirmView 逐项确认)→
4827
+ * 确认导入(apply-items)→ 执行结果 + 一键回滚(restoreId);「选择历史快照」下拉;
4828
+ * - 【方案 A】自动同步设置区块:总开关 + 间隔下拉 + 状态(上次运行 / 下次倒计时);
4081
4829
  * - 状态行:凭据配置 + 上次同步时间 + 通道(来自 GET /sync/status,组件挂载时加载)。
4082
4830
  *
4083
4831
  * 全部渲染模型来自 ./sync-view.ts 纯函数(node 单测覆盖),组件只做装配;
@@ -4092,6 +4840,15 @@ const initialGithub = {
4092
4840
  interval: 5,
4093
4841
  error: null
4094
4842
  };
4843
+ const AUTOSYNC_INTERVAL_OPTIONS = [
4844
+ "5m",
4845
+ "15m",
4846
+ "30m",
4847
+ "60m",
4848
+ "6h",
4849
+ "12h",
4850
+ "24h"
4851
+ ];
4095
4852
  const initial = {
4096
4853
  loading: true,
4097
4854
  loadError: null,
@@ -4102,6 +4859,13 @@ const initial = {
4102
4859
  busy: null,
4103
4860
  pushReport: null,
4104
4861
  pullReport: null,
4862
+ confirmSession: null,
4863
+ snapshots: [],
4864
+ selectedSnapshotId: "",
4865
+ autosync: null,
4866
+ autosyncEnabled: false,
4867
+ autosyncInterval: "30m",
4868
+ lastRestoreId: null,
4105
4869
  error: null,
4106
4870
  github: initialGithub
4107
4871
  };
@@ -4121,7 +4885,7 @@ function SyncSettingsView({ api, t }) {
4121
4885
  }));
4122
4886
  /** GitHub 轮询定时器(卸载/取消时清理,防止泄漏与跨流程串扰) */
4123
4887
  const githubPollTimer = (0, react.useRef)(null);
4124
- /** 挂载时读取同步状态(配置回填 + 上次同步时间 + 凭据状态) */
4888
+ /** 挂载时读取同步状态(配置回填 + 上次同步时间 + 凭据状态 + autosync) */
4125
4889
  const loadStatus = async () => {
4126
4890
  patch({
4127
4891
  loading: true,
@@ -4133,8 +4897,14 @@ function SyncSettingsView({ api, t }) {
4133
4897
  loading: false,
4134
4898
  statusInfo: info,
4135
4899
  repoUrl: info.repoUrl ?? "",
4136
- gitBin: info.gitBin ?? ""
4900
+ gitBin: info.gitBin ?? "",
4901
+ ...info.autosync !== void 0 ? {
4902
+ autosync: info.autosync,
4903
+ autosyncEnabled: info.autosync.enabled,
4904
+ autosyncInterval: info.autosync.interval
4905
+ } : {}
4137
4906
  });
4907
+ if (info.autosync === void 0) loadAutosync();
4138
4908
  } catch (err) {
4139
4909
  patch({
4140
4910
  loading: false,
@@ -4142,6 +4912,26 @@ function SyncSettingsView({ api, t }) {
4142
4912
  });
4143
4913
  }
4144
4914
  };
4915
+ /** 读取自动同步状态(GET /sync/autosync)。 */
4916
+ const loadAutosync = async () => {
4917
+ try {
4918
+ const autosync = await api.autosyncStatus();
4919
+ patch({
4920
+ autosync,
4921
+ autosyncEnabled: autosync.enabled,
4922
+ autosyncInterval: autosync.interval
4923
+ });
4924
+ } catch (err) {
4925
+ patch({ error: err instanceof Error ? err.message : String(err) });
4926
+ }
4927
+ };
4928
+ /** 读取远端历史快照列表(「选择历史快照」下拉数据源)。 */
4929
+ const loadSnapshots = async () => {
4930
+ try {
4931
+ const res = await api.snapshotsList(payload());
4932
+ patch({ snapshots: res.snapshots });
4933
+ } catch {}
4934
+ };
4145
4935
  (0, react.useEffect)(() => {
4146
4936
  loadStatus();
4147
4937
  }, []);
@@ -4267,6 +5057,88 @@ function SyncSettingsView({ api, t }) {
4267
5057
  });
4268
5058
  }
4269
5059
  };
5060
+ /** 一键同步:拉取 → 差异确认会话(先取消旧会话,再发起新会话)。 */
5061
+ const runSync = async (snapshotId) => {
5062
+ if (state.confirmSession !== null) try {
5063
+ await api.cancel(state.confirmSession.syncSessionId);
5064
+ } catch {}
5065
+ patch({
5066
+ busy: "sync",
5067
+ error: null,
5068
+ confirmSession: null,
5069
+ lastRestoreId: null
5070
+ });
5071
+ try {
5072
+ const session = await api.sync({
5073
+ ...payload(),
5074
+ ...snapshotId !== void 0 && snapshotId !== "" ? { snapshotId } : {}
5075
+ });
5076
+ if (!session.ok) {
5077
+ patch({
5078
+ busy: null,
5079
+ error: session.message ?? t("syncflow.syncFailed")
5080
+ });
5081
+ return;
5082
+ }
5083
+ patch({
5084
+ busy: null,
5085
+ confirmSession: session,
5086
+ token: ""
5087
+ });
5088
+ loadSnapshots();
5089
+ } catch (err) {
5090
+ patch({
5091
+ busy: null,
5092
+ error: err instanceof Error ? err.message : String(err)
5093
+ });
5094
+ }
5095
+ };
5096
+ /** 用户取消差异确认:清除会话,复位到空闲。 */
5097
+ const cancelConfirm = () => {
5098
+ patch({ confirmSession: null });
5099
+ };
5100
+ /** 从 SyncConfirmView 透传的一键回滚完成信号。 */
5101
+ const onRollbackApplied = () => {
5102
+ patch({ lastRestoreId: null });
5103
+ };
5104
+ const toggleAutosync = async (enabled) => {
5105
+ patch({
5106
+ autosyncEnabled: enabled,
5107
+ error: null
5108
+ });
5109
+ try {
5110
+ const updated = await api.autosyncUpdate({
5111
+ enabled,
5112
+ interval: state.autosyncInterval
5113
+ });
5114
+ patch({
5115
+ autosync: updated,
5116
+ autosyncEnabled: updated.enabled,
5117
+ autosyncInterval: updated.interval
5118
+ });
5119
+ } catch (err) {
5120
+ patch({ error: err instanceof Error ? err.message : String(err) });
5121
+ }
5122
+ };
5123
+ const updateAutosyncInterval = async (interval) => {
5124
+ patch({
5125
+ autosyncInterval: interval,
5126
+ error: null
5127
+ });
5128
+ try {
5129
+ const updated = await api.autosyncUpdate({
5130
+ enabled: state.autosyncEnabled,
5131
+ interval
5132
+ });
5133
+ patch({
5134
+ autosync: updated,
5135
+ autosyncEnabled: updated.enabled,
5136
+ autosyncInterval: updated.interval
5137
+ });
5138
+ } catch (err) {
5139
+ patch({ error: err instanceof Error ? err.message : String(err) });
5140
+ }
5141
+ };
4270
5142
  const status = computeSyncStatus(state.statusInfo, state.loading, state.loadError, uiT);
4271
5143
  const buttons = computeSyncButtons(state.busy, state.repoUrl, uiT);
4272
5144
  const pushView = pushReportView(state.pushReport, uiT);
@@ -4274,6 +5146,8 @@ function SyncSettingsView({ api, t }) {
4274
5146
  const githubView = computeGithubLoginView(state.github.phase, state.github.userCode, state.github.verificationUri, state.github.error, uiT);
4275
5147
  /** GitHub 流程进行中(请求设备码 / 等待授权 / 轮询):禁用 push/pull,避免无凭据操作 */
4276
5148
  const githubBusy = state.github.phase === "starting" || state.github.phase === "waiting" || state.github.phase === "polling";
5149
+ const autosyncText = state.autosync !== null ? autosyncStatusText(state.autosync, uiT) : t("autosync.statusNever");
5150
+ const autosyncCountdown = state.autosync !== null && state.autosync.elapsedMs >= 0 ? formatIntervalDuration(computeAutosyncCountdown(state.autosync.elapsedMs, autosyncIntervalMs(state.autosync.interval)), uiT) : null;
4277
5151
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4278
5152
  className: config_manager_module_css_default.viewBody,
4279
5153
  children: [
@@ -4332,7 +5206,7 @@ function SyncSettingsView({ api, t }) {
4332
5206
  className: config_manager_module_css_default.input,
4333
5207
  value: state.token,
4334
5208
  autoComplete: "off",
4335
- placeholder: "ghp_…(可选)",
5209
+ placeholder: t("config.tokenPlaceholder"),
4336
5210
  disabled: state.busy !== null,
4337
5211
  onChange: (e) => {
4338
5212
  patch({ token: e.target.value });
@@ -4434,22 +5308,79 @@ function SyncSettingsView({ api, t }) {
4434
5308
  })] }),
4435
5309
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4436
5310
  className: config_manager_module_css_default.actionRow,
4437
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
4438
- variant: "primary",
4439
- disabled: !buttons.canPush || githubBusy,
4440
- onClick: () => {
4441
- runPush();
4442
- },
4443
- children: state.busy === "push" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Spinner, { label: buttons.pushLabel }) : buttons.pushLabel
4444
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
4445
- disabled: !buttons.canPull || githubBusy,
4446
- onClick: () => {
4447
- runPull();
4448
- },
4449
- children: state.busy === "pull" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Spinner, { label: buttons.pullLabel }) : buttons.pullLabel
4450
- })]
5311
+ children: [
5312
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
5313
+ variant: "primary",
5314
+ disabled: state.busy !== null || state.repoUrl.trim() === "",
5315
+ onClick: () => {
5316
+ runSync();
5317
+ },
5318
+ children: state.busy === "sync" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Spinner, { label: t("syncflow.syncing") }) : t("syncflow.button")
5319
+ }),
5320
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
5321
+ disabled: !buttons.canPush || githubBusy,
5322
+ onClick: () => {
5323
+ runPush();
5324
+ },
5325
+ children: state.busy === "push" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Spinner, { label: buttons.pushLabel }) : buttons.pushLabel
5326
+ }),
5327
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
5328
+ disabled: !buttons.canPull || githubBusy,
5329
+ onClick: () => {
5330
+ runPull();
5331
+ },
5332
+ children: state.busy === "pull" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Spinner, { label: buttons.pullLabel }) : buttons.pullLabel
5333
+ })
5334
+ ]
5335
+ }),
5336
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5337
+ className: config_manager_module_css_default.statRow,
5338
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
5339
+ className: config_manager_module_css_default.field,
5340
+ children: [
5341
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5342
+ className: config_manager_module_css_default.fieldLabel,
5343
+ children: t("syncflow.selectSnapshot")
5344
+ }),
5345
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
5346
+ className: config_manager_module_css_default.input,
5347
+ value: state.selectedSnapshotId,
5348
+ disabled: state.busy !== null,
5349
+ onChange: (e) => {
5350
+ const id = e.target.value;
5351
+ patch({ selectedSnapshotId: id });
5352
+ runSync(id === "" ? void 0 : id);
5353
+ },
5354
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
5355
+ value: "",
5356
+ children: t("syncflow.latestSnapshot")
5357
+ }), state.snapshots.map((s) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
5358
+ value: s.id,
5359
+ children: [s.id, t("syncflow.snapshotOption", {
5360
+ date: s.createdAt.slice(0, 10),
5361
+ count: String(s.sectionCount)
5362
+ })]
5363
+ }, s.id))]
5364
+ }),
5365
+ state.snapshots.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5366
+ className: config_manager_module_css_default.hint,
5367
+ children: t("syncflow.noSnapshots")
5368
+ })
5369
+ ]
5370
+ })
4451
5371
  }),
4452
5372
  state.error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBanner, { error: state.error }),
5373
+ state.confirmSession !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SyncConfirmView, {
5374
+ api,
5375
+ syncSessionId: state.confirmSession.syncSessionId,
5376
+ snapshotId: state.confirmSession.snapshotId,
5377
+ items: state.confirmSession.items,
5378
+ needsReview: state.confirmSession.needsReview,
5379
+ compatibility: state.confirmSession.compatibility,
5380
+ t,
5381
+ onCancel: cancelConfirm,
5382
+ onRollbackDone: onRollbackApplied
5383
+ }),
4453
5384
  pushView !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Card, { children: [
4454
5385
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4455
5386
  className: config_manager_module_css_default.groupLabel,
@@ -4546,10 +5477,83 @@ function SyncSettingsView({ api, t }) {
4546
5477
  kind: "info",
4547
5478
  children: pullView.previewHint
4548
5479
  })
4549
- ] })
5480
+ ] }),
5481
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Card, { children: [
5482
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5483
+ className: config_manager_module_css_default.groupLabel,
5484
+ children: t("autosync.title")
5485
+ }),
5486
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5487
+ className: config_manager_module_css_default.hint,
5488
+ children: t("autosync.description")
5489
+ }),
5490
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
5491
+ className: config_manager_module_css_default.checkboxRow,
5492
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
5493
+ type: "checkbox",
5494
+ checked: state.autosyncEnabled,
5495
+ disabled: state.busy !== null,
5496
+ onChange: (e) => {
5497
+ toggleAutosync(e.target.checked);
5498
+ }
5499
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("autosync.enable") })]
5500
+ }),
5501
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
5502
+ className: config_manager_module_css_default.field,
5503
+ children: [
5504
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5505
+ className: config_manager_module_css_default.fieldLabel,
5506
+ children: t("autosync.interval")
5507
+ }),
5508
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
5509
+ className: config_manager_module_css_default.input,
5510
+ value: state.autosyncInterval,
5511
+ disabled: state.busy !== null,
5512
+ onChange: (e) => {
5513
+ updateAutosyncInterval(e.target.value);
5514
+ },
5515
+ children: AUTOSYNC_INTERVAL_OPTIONS.map((iv) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
5516
+ value: iv,
5517
+ children: intervalLabel(iv, t)
5518
+ }, iv))
5519
+ }),
5520
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5521
+ className: config_manager_module_css_default.hint,
5522
+ children: t("autosync.intervalHint")
5523
+ })
5524
+ ]
5525
+ }),
5526
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5527
+ className: config_manager_module_css_default.statRow,
5528
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
5529
+ kind: state.autosync?.lastRunStatus === "failed" ? "error" : state.autosync?.lastRunStatus === "skipped" ? "warn" : "info",
5530
+ children: autosyncText
5531
+ }), autosyncCountdown !== null && state.autosyncEnabled && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
5532
+ kind: "info",
5533
+ children: t("autosync.nextRun", { time: autosyncCountdown })
5534
+ })]
5535
+ })
5536
+ ] }),
5537
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SyncHistoryView, {
5538
+ api,
5539
+ t
5540
+ })
4550
5541
  ]
4551
5542
  });
4552
5543
  }
5544
+ /** AutosyncInterval → 可读标签(复用 i18n interval 键)。 */
5545
+ function intervalLabel(iv, t) {
5546
+ switch (iv) {
5547
+ case "5m": return t("autosync.interval5m");
5548
+ case "15m": return t("autosync.interval15m");
5549
+ case "30m": return t("autosync.interval30m");
5550
+ case "60m": return t("autosync.interval60m");
5551
+ case "6h": return t("autosync.interval6h");
5552
+ case "12h": return t("autosync.interval12h");
5553
+ case "24h": return t("autosync.interval24h");
5554
+ default: return iv;
5555
+ }
5556
+ }
4553
5557
  //#endregion
4554
5558
  //#region src/client/ConfigManagerSection.tsx
4555
5559
  /**
@@ -4906,6 +5910,7 @@ const zh = {
4906
5910
  "config.token": "认证 token",
4907
5911
  "config.tokenHint": "将安全写入 DSH credentials(引用名 {ref}),不会写入同步文件或日志。留空表示沿用已保存的凭据。",
4908
5912
  "config.tokenSaved": "凭据已配置",
5913
+ "config.tokenPlaceholder": "ghp_…(可选)",
4909
5914
  "config.gitBin": "git 可执行文件(可选)",
4910
5915
  "config.gitBinHint": "缺省使用 PATH 中的 git。",
4911
5916
  "github.title": "GitHub 登录",
@@ -4930,6 +5935,84 @@ const zh = {
4930
5935
  "change.total": "共 {total} 项变更",
4931
5936
  "sections.title": "同步分区",
4932
5937
  "warnings.title": "分区告警",
5938
+ "syncflow.title": "一键同步",
5939
+ "syncflow.button": "一键同步",
5940
+ "syncflow.syncing": "正在同步…",
5941
+ "syncflow.syncFailed": "同步失败",
5942
+ "syncflow.snapshotOption": "({date} · {count} 分区)",
5943
+ "syncflow.selectSnapshot": "选择历史快照",
5944
+ "syncflow.latestSnapshot": "最新快照",
5945
+ "syncflow.noSnapshots": "远端暂无快照",
5946
+ "syncflow.confirmImport": "确认导入",
5947
+ "syncflow.cancel": "取消",
5948
+ "syncflow.adoptRemote": "采用远端",
5949
+ "syncflow.adoptHint": "勾选 = 导入该项;取消 = 跳过",
5950
+ "syncflow.needsReviewBadge": "需人工决策",
5951
+ "syncflow.empty": "远端快照与本地一致(无变更)",
5952
+ "syncflow.diffCount": "共 {count} 项差异",
5953
+ "syncflow.conflictTitle": "冲突解决",
5954
+ "syncflow.conflictUseLocal": "用本地",
5955
+ "syncflow.conflictUseRemote": "用远端",
5956
+ "syncflow.conflictSkip": "跳过",
5957
+ "syncflow.conflictLocalLabel": "本地",
5958
+ "syncflow.conflictRemoteLabel": "远端",
5959
+ "syncflow.conflictAncestorLabel": "共同祖先",
5960
+ "syncflow.conflictUnresolved": "冲突项 {itemId} 尚未选择解决方式(用本地 / 用远端 / 跳过)",
5961
+ "syncflow.diff": "差异",
5962
+ "syncflow.importDone": "已导入 {n} 个分区",
5963
+ "syncflow.importFailed": "导入失败(已整体回滚)",
5964
+ "syncflow.importedSections": "已写入",
5965
+ "syncflow.importWarnings": "导入告警",
5966
+ "syncflow.rollback": "回滚到应用前",
5967
+ "syncflow.rollingBack": "正在回滚…",
5968
+ "syncflow.rollbackDone": "已回滚到应用前",
5969
+ "syncflow.needsRestart": "部分改动需要重启 DSH 后生效",
5970
+ "autosync.title": "自动同步",
5971
+ "autosync.description": "开启后,DSH 将按固定间隔自动执行双向同步(拉取合并 + 上传);DSH 启动时还会触发一次「自动下载合并(不上传)」以保持本地为最新。仅在无冲突且无需人工干预时才自动写入。",
5972
+ "autosync.enable": "启用自动同步",
5973
+ "autosync.interval": "同步间隔",
5974
+ "autosync.intervalHint": "DSH 启动时自动下载合并不上传。",
5975
+ "autosync.interval5m": "5 分钟",
5976
+ "autosync.interval15m": "15 分钟",
5977
+ "autosync.interval30m": "30 分钟",
5978
+ "autosync.interval60m": "60 分钟",
5979
+ "autosync.interval6h": "6 小时",
5980
+ "autosync.interval12h": "12 小时",
5981
+ "autosync.interval24h": "24 小时",
5982
+ "autosync.status": "上次运行:{status}({time})",
5983
+ "autosync.statusNever": "从未运行",
5984
+ "autosync.nextRun": "下次约 {time} 后",
5985
+ "autosync.failCount": "连续失败 {n} 次",
5986
+ "autosync.running": "自动同步进行中…",
5987
+ "autosync.success": "成功",
5988
+ "autosync.skipped": "已跳过",
5989
+ "autosync.failed": "失败",
5990
+ "autosync.partial": "部分成功",
5991
+ "history.title": "同步历史",
5992
+ "history.empty": "尚无同步历史",
5993
+ "history.emptyHint": "完成首次 push / 一键同步 / 自动同步后,这里会显示记录。",
5994
+ "history.colTime": "时间",
5995
+ "history.colKind": "类型",
5996
+ "history.colDetail": "详情",
5997
+ "history.kindSnapshot": "快照",
5998
+ "history.kindAutosync": "自动同步",
5999
+ "history.sectionCount": "分区",
6000
+ "history.detail": "明细",
6001
+ "history.autosyncDirection": "方向:{direction}",
6002
+ "history.autosyncPull": "下载",
6003
+ "history.autosyncPush": "上传",
6004
+ "history.autosyncBoth": "双向",
6005
+ "history.autosyncSkipReason": "跳过原因:{reason}",
6006
+ "history.autosyncReasonConflict": "冲突",
6007
+ "history.autosyncReasonNoRemote": "无远端快照",
6008
+ "history.autosyncReasonNotConfigured": "未配置仓库",
6009
+ "history.autosyncReasonNetwork": "网络问题",
6010
+ "history.autosyncConflicted": "跳过冲突分区:{sections}",
6011
+ "history.autosyncApplied": "应用分区:{sections}",
6012
+ "history.autosyncError": "错误:{error}",
6013
+ "history.autosyncNotified": "已通知(连续失败 3 次)",
6014
+ "history.column.snapshot": "快照",
6015
+ "history.column.autosync": "自动同步",
4933
6016
  "common.close": "关闭",
4934
6017
  "common.retry": "重试",
4935
6018
  "common.loading": "加载中…"
@@ -4944,6 +6027,7 @@ const en = {
4944
6027
  "config.token": "Auth token",
4945
6028
  "config.tokenHint": "Securely written into DSH credentials (ref {ref}); never written into sync files or logs. Leave blank to reuse the saved credential.",
4946
6029
  "config.tokenSaved": "Credential configured",
6030
+ "config.tokenPlaceholder": "ghp_… (optional)",
4947
6031
  "config.gitBin": "git executable (optional)",
4948
6032
  "config.gitBinHint": "Defaults to git on PATH.",
4949
6033
  "github.title": "GitHub Sign-in",
@@ -4968,6 +6052,84 @@ const en = {
4968
6052
  "change.total": "{total} change(s)",
4969
6053
  "sections.title": "Synced Sections",
4970
6054
  "warnings.title": "Section Warnings",
6055
+ "syncflow.title": "One-Click Sync",
6056
+ "syncflow.button": "One-click sync",
6057
+ "syncflow.syncing": "Syncing…",
6058
+ "syncflow.syncFailed": "Sync failed",
6059
+ "syncflow.snapshotOption": "({date} · {count} sections)",
6060
+ "syncflow.selectSnapshot": "Select snapshot",
6061
+ "syncflow.latestSnapshot": "Latest snapshot",
6062
+ "syncflow.noSnapshots": "No remote snapshots",
6063
+ "syncflow.confirmImport": "Confirm import",
6064
+ "syncflow.cancel": "Cancel",
6065
+ "syncflow.adoptRemote": "Adopt remote",
6066
+ "syncflow.adoptHint": "Check = import item; uncheck = skip",
6067
+ "syncflow.needsReviewBadge": "Needs decision",
6068
+ "syncflow.empty": "Remote snapshot matches local (no changes)",
6069
+ "syncflow.diffCount": "{count} change(s)",
6070
+ "syncflow.conflictTitle": "Resolve conflict",
6071
+ "syncflow.conflictUseLocal": "Use local",
6072
+ "syncflow.conflictUseRemote": "Use remote",
6073
+ "syncflow.conflictSkip": "Skip",
6074
+ "syncflow.conflictLocalLabel": "Local",
6075
+ "syncflow.conflictRemoteLabel": "Remote",
6076
+ "syncflow.conflictAncestorLabel": "Common ancestor",
6077
+ "syncflow.conflictUnresolved": "Conflict item {itemId} has no resolution yet (use local / use remote / skip)",
6078
+ "syncflow.diff": "Diff",
6079
+ "syncflow.importDone": "Imported {n} section(s)",
6080
+ "syncflow.importFailed": "Import failed (rolled back)",
6081
+ "syncflow.importedSections": "Written",
6082
+ "syncflow.importWarnings": "Import warnings",
6083
+ "syncflow.rollback": "Rollback",
6084
+ "syncflow.rollingBack": "Rolling back…",
6085
+ "syncflow.rollbackDone": "Rolled back",
6086
+ "syncflow.needsRestart": "Some changes require a DSH restart to take effect",
6087
+ "autosync.title": "Auto Sync",
6088
+ "autosync.description": "When enabled, DSH will run two-way sync (pull merge + push) on a fixed interval; on startup it also triggers a download-merge (no upload) to keep local up to date. Local writes happen only when there are no conflicts or manual-decision items.",
6089
+ "autosync.enable": "Enable auto sync",
6090
+ "autosync.interval": "Sync interval",
6091
+ "autosync.intervalHint": "Auto download-merge on DSH startup, no upload.",
6092
+ "autosync.interval5m": "5 min",
6093
+ "autosync.interval15m": "15 min",
6094
+ "autosync.interval30m": "30 min",
6095
+ "autosync.interval60m": "60 min",
6096
+ "autosync.interval6h": "6 hr",
6097
+ "autosync.interval12h": "12 hr",
6098
+ "autosync.interval24h": "24 hr",
6099
+ "autosync.status": "Last run: {status} ({time})",
6100
+ "autosync.statusNever": "Never run",
6101
+ "autosync.nextRun": "Next in ~{time}",
6102
+ "autosync.failCount": "{n} consecutive failure(s)",
6103
+ "autosync.running": "Auto sync running…",
6104
+ "autosync.success": "Success",
6105
+ "autosync.skipped": "Skipped",
6106
+ "autosync.failed": "Failed",
6107
+ "autosync.partial": "Partial",
6108
+ "history.title": "Sync History",
6109
+ "history.empty": "No sync history yet",
6110
+ "history.emptyHint": "Records appear here after your first push / one-click sync / auto sync.",
6111
+ "history.colTime": "Time",
6112
+ "history.colKind": "Kind",
6113
+ "history.colDetail": "Detail",
6114
+ "history.kindSnapshot": "Snapshot",
6115
+ "history.kindAutosync": "Auto Sync",
6116
+ "history.sectionCount": "sections",
6117
+ "history.detail": "Detail",
6118
+ "history.autosyncDirection": "Direction: {direction}",
6119
+ "history.autosyncPull": "Pull",
6120
+ "history.autosyncPush": "Push",
6121
+ "history.autosyncBoth": "Both",
6122
+ "history.autosyncSkipReason": "Skip reason: {reason}",
6123
+ "history.autosyncReasonConflict": "Conflict",
6124
+ "history.autosyncReasonNoRemote": "No remote snapshot",
6125
+ "history.autosyncReasonNotConfigured": "No repository configured",
6126
+ "history.autosyncReasonNetwork": "Network issue",
6127
+ "history.autosyncConflicted": "Skipped conflict sections: {sections}",
6128
+ "history.autosyncApplied": "Applied sections: {sections}",
6129
+ "history.autosyncError": "Error: {error}",
6130
+ "history.autosyncNotified": "Notified (3 consecutive failures)",
6131
+ "history.column.snapshot": "Snapshot",
6132
+ "history.column.autosync": "Auto Sync",
4971
6133
  "common.close": "Close",
4972
6134
  "common.retry": "Retry",
4973
6135
  "common.loading": "Loading…"