dsh-config-manager 0.1.20 → 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 (54) hide show
  1. package/lib/client.d.ts +312 -46
  2. package/lib/client.js +1172 -205
  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 +292 -19
  8. package/lib/index.js.map +1 -1
  9. package/lib/sync/autosync-config.d.ts +34 -0
  10. package/lib/sync/autosync-config.js +116 -0
  11. package/lib/sync/autosync-config.js.map +1 -0
  12. package/lib/sync/autosync-scheduler.d.ts +95 -0
  13. package/lib/sync/autosync-scheduler.js +379 -0
  14. package/lib/sync/autosync-scheduler.js.map +1 -0
  15. package/lib/sync/sync-engine.d.ts +59 -4
  16. package/lib/sync/sync-engine.js +138 -12
  17. package/lib/sync/sync-engine.js.map +1 -1
  18. package/lib/sync/sync-history.d.ts +39 -0
  19. package/lib/sync/sync-history.js +88 -0
  20. package/lib/sync/sync-history.js.map +1 -0
  21. package/lib/sync/sync-session.d.ts +39 -0
  22. package/lib/sync/sync-session.js +54 -0
  23. package/lib/sync/sync-session.js.map +1 -0
  24. package/lib/ui/i18n.d.ts +63 -0
  25. package/lib/ui/i18n.js +130 -1
  26. package/lib/ui/i18n.js.map +1 -1
  27. package/package.json +1 -1
  28. package/src/client/config-manager.module.css +18 -0
  29. package/src/client/sync/SyncConfirmView.tsx +301 -0
  30. package/src/client/sync/SyncHistoryView.tsx +107 -25
  31. package/src/client/sync/SyncSettingsView.tsx +226 -83
  32. package/src/client/sync/history-model.test.ts +82 -0
  33. package/src/client/sync/history-model.ts +89 -2
  34. package/src/client/sync/sync-api.test.ts +155 -0
  35. package/src/client/sync/sync-api.ts +208 -16
  36. package/src/client/sync/sync-locales.ts +162 -1
  37. package/src/client/sync/sync-view-v2.test.ts +131 -0
  38. package/src/client/sync/sync-view.ts +151 -4
  39. package/src/core/run-registry.ts +7 -3
  40. package/src/index.ts +314 -22
  41. package/src/sync/autosync-config.test.ts +93 -0
  42. package/src/sync/autosync-config.ts +137 -0
  43. package/src/sync/autosync-scheduler.test.ts +191 -0
  44. package/src/sync/autosync-scheduler.ts +443 -0
  45. package/src/sync/sync-engine.test.ts +107 -7
  46. package/src/sync/sync-engine.ts +176 -14
  47. package/src/sync/sync-history.test.ts +85 -0
  48. package/src/sync/sync-history.ts +126 -0
  49. package/src/sync/sync-session.test.ts +137 -0
  50. package/src/sync/sync-session.ts +76 -0
  51. package/src/ui/i18n.ts +132 -3
  52. package/src/client/sync/SyncPullPreviewView.test.ts +0 -54
  53. package/src/client/sync/SyncPullPreviewView.tsx +0 -165
  54. package/src/client/sync/pull-preview-model.ts +0 -44
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 = {
2078
+ "categoryItem": "_367UGW_categoryItem",
1952
2079
  "warnText": "_367UGW_warnText",
1953
- "checkboxRow": "_367UGW_checkboxRow",
1954
- "progressDetail": "_367UGW_progressDetail",
1955
- "conflictDetail": "_367UGW_conflictDetail",
1956
- "kindTag": "_367UGW_kindTag",
1957
- "modeTab": "_367UGW_modeTab",
1958
- "groupList": "_367UGW_groupList",
1959
- "progressBadgeSection": "_367UGW_progressBadgeSection",
2080
+ "rowActions": "_367UGW_rowActions",
1960
2081
  "groupLabel": "_367UGW_groupLabel",
1961
- "fieldLabel": "_367UGW_fieldLabel",
1962
- "groupCard": "_367UGW_groupCard",
1963
- "hiddenFile": "_367UGW_hiddenFile",
1964
- "conflictId": "_367UGW_conflictId",
1965
- "spinner": "_367UGW_spinner",
1966
- "section": "_367UGW_section",
1967
- "progressMeta": "_367UGW_progressMeta",
1968
- "reportScroll": "_367UGW_reportScroll",
1969
- "dshCmIndeterminate": "_367UGW_dshCmIndeterminate",
1970
- "badgeOk": "_367UGW_badgeOk",
1971
- "banner": "_367UGW_banner",
2082
+ "groupNote": "_367UGW_groupNote",
1972
2083
  "warnList": "_367UGW_warnList",
2084
+ "secretsList": "_367UGW_secretsList",
1973
2085
  "progressBadge": "_367UGW_progressBadge",
1974
- "spinnerLabel": "_367UGW_spinnerLabel",
1975
- "primaryButton": "_367UGW_primaryButton",
2086
+ "viewBody": "_367UGW_viewBody",
2087
+ "progressPercent": "_367UGW_progressPercent",
2088
+ "sectionHeader": "_367UGW_sectionHeader",
2089
+ "field": "_367UGW_field",
2090
+ "categoryDesc": "_367UGW_categoryDesc",
2091
+ "pathIssueKind": "_367UGW_pathIssueKind",
1976
2092
  "progressBarDone": "_367UGW_progressBarDone",
1977
- "secretFields": "_367UGW_secretFields",
1978
- "progressBadgeCount": "_367UGW_progressBadgeCount",
1979
- "pathValue": "_367UGW_pathValue",
1980
- "groupItems": "_367UGW_groupItems",
1981
- "errorAction": "_367UGW_errorAction",
1982
- "planScroll": "_367UGW_planScroll",
1983
- "progressBlock": "_367UGW_progressBlock",
1984
- "formError": "_367UGW_formError",
2093
+ "modeTabs": "_367UGW_modeTabs",
2094
+ "reportView": "_367UGW_reportView",
2095
+ "snapshotRow": "_367UGW_snapshotRow",
2096
+ "dshCmIndeterminate": "_367UGW_dshCmIndeterminate",
2097
+ "snapshotRowHeader": "_367UGW_snapshotRowHeader",
2098
+ "errorLine": "_367UGW_errorLine",
2099
+ "banner": "_367UGW_banner",
1985
2100
  "radioLabel": "_367UGW_radioLabel",
1986
- "badge": "_367UGW_badge",
1987
- "categoryDesc": "_367UGW_categoryDesc",
2101
+ "progressMeta": "_367UGW_progressMeta",
2102
+ "groupItems": "_367UGW_groupItems",
2103
+ "pathMappingList": "_367UGW_pathMappingList",
2104
+ "sectionTitle": "_367UGW_sectionTitle",
2105
+ "pathNew": "_367UGW_pathNew",
2106
+ "reportList": "_367UGW_reportList",
1988
2107
  "dangerButton": "_367UGW_dangerButton",
1989
- "progressPercent": "_367UGW_progressPercent",
1990
- "progressBar": "_367UGW_progressBar",
1991
- "ghostButton": "_367UGW_ghostButton",
1992
- "errorBanner": "_367UGW_errorBanner",
1993
- "categoryItem": "_367UGW_categoryItem",
1994
- "errorList": "_367UGW_errorList",
1995
- "reportText": "_367UGW_reportText",
1996
- "conflictList": "_367UGW_conflictList",
1997
2108
  "errorReason": "_367UGW_errorReason",
1998
- "severityError": "_367UGW_severityError",
1999
- "rowActions": "_367UGW_rowActions",
2109
+ "viewTab": "_367UGW_viewTab",
2110
+ "conflictItem": "_367UGW_conflictItem",
2111
+ "diffScroll": "_367UGW_diffScroll",
2112
+ "progressBadgeCount": "_367UGW_progressBadgeCount",
2113
+ "formError": "_367UGW_formError",
2114
+ "card": "_367UGW_card",
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",
2000
2122
  "rollbackBox": "_367UGW_rollbackBox",
2001
- "badgeError": "_367UGW_badgeError",
2123
+ "badgeOk": "_367UGW_badgeOk",
2124
+ "progressBar": "_367UGW_progressBar",
2125
+ "progressLabel": "_367UGW_progressLabel",
2126
+ "badge": "_367UGW_badge",
2127
+ "errorAction": "_367UGW_errorAction",
2002
2128
  "badgeInfo": "_367UGW_badgeInfo",
2003
- "optionsCard": "_367UGW_optionsCard",
2004
- "dshCmSpin": "_367UGW_dshCmSpin",
2005
- "input": "_367UGW_input",
2006
2129
  "errorItem": "_367UGW_errorItem",
2007
- "groupHeader": "_367UGW_groupHeader",
2008
- "spinnerWrap": "_367UGW_spinnerWrap",
2009
- "pathOld": "_367UGW_pathOld",
2130
+ "conflictHead": "_367UGW_conflictHead",
2131
+ "severityError": "_367UGW_severityError",
2132
+ "fieldLabel": "_367UGW_fieldLabel",
2133
+ "actionRow": "_367UGW_actionRow",
2134
+ "errorBanner": "_367UGW_errorBanner",
2135
+ "empty": "_367UGW_empty",
2136
+ "spinner": "_367UGW_spinner",
2137
+ "kindTag": "_367UGW_kindTag",
2138
+ "spinnerLabel": "_367UGW_spinnerLabel",
2139
+ "input": "_367UGW_input",
2140
+ "errorActionLabel": "_367UGW_errorActionLabel",
2141
+ "planScroll": "_367UGW_planScroll",
2142
+ "errorList": "_367UGW_errorList",
2143
+ "progressDetail": "_367UGW_progressDetail",
2010
2144
  "snapshotList": "_367UGW_snapshotList",
2011
- "viewTab": "_367UGW_viewTab",
2145
+ "ghostButton": "_367UGW_ghostButton",
2146
+ "hint": "_367UGW_hint",
2147
+ "progressIndeterminate": "_367UGW_progressIndeterminate",
2148
+ "conflictDetail": "_367UGW_conflictDetail",
2149
+ "pathValue": "_367UGW_pathValue",
2150
+ "badgeError": "_367UGW_badgeError",
2012
2151
  "sectionSubtitle": "_367UGW_sectionSubtitle",
2013
- "sectionBody": "_367UGW_sectionBody",
2014
- "actionRow": "_367UGW_actionRow",
2015
- "modeHint": "_367UGW_modeHint",
2016
- "statRow": "_367UGW_statRow",
2152
+ "viewTabs": "_367UGW_viewTabs",
2153
+ "modeTab": "_367UGW_modeTab",
2017
2154
  "categoryName": "_367UGW_categoryName",
2018
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",
2019
2163
  "reportFooter": "_367UGW_reportFooter",
2020
- "progressIndeterminate": "_367UGW_progressIndeterminate",
2021
- "sectionHeader": "_367UGW_sectionHeader",
2164
+ "pathOld": "_367UGW_pathOld",
2022
2165
  "badgeWarn": "_367UGW_badgeWarn",
2023
- "conflictHead": "_367UGW_conflictHead",
2024
- "conflictOptions": "_367UGW_conflictOptions",
2025
- "viewBody": "_367UGW_viewBody",
2026
- "pathIssueKind": "_367UGW_pathIssueKind",
2027
- "snapshotRowHeader": "_367UGW_snapshotRowHeader",
2028
- "snapshotRow": "_367UGW_snapshotRow",
2029
- "reportList": "_367UGW_reportList",
2030
- "empty": "_367UGW_empty",
2031
- "field": "_367UGW_field",
2032
- "sectionTitle": "_367UGW_sectionTitle",
2033
- "errorFooter": "_367UGW_errorFooter",
2034
- "pathMappingList": "_367UGW_pathMappingList",
2035
- "reportView": "_367UGW_reportView",
2036
- "secretsList": "_367UGW_secretsList",
2037
- "pathRow": "_367UGW_pathRow",
2038
- "groupNote": "_367UGW_groupNote",
2039
- "card": "_367UGW_card",
2166
+ "section": "_367UGW_section",
2167
+ "reportScroll": "_367UGW_reportScroll",
2040
2168
  "sectionTitleBlock": "_367UGW_sectionTitleBlock",
2041
- "conflictItem": "_367UGW_conflictItem",
2042
- "modeTabs": "_367UGW_modeTabs",
2043
- "viewTabs": "_367UGW_viewTabs",
2044
- "errorTitle": "_367UGW_errorTitle",
2045
- "errorLine": "_367UGW_errorLine",
2046
- "pathNew": "_367UGW_pathNew",
2047
- "progressLabel": "_367UGW_progressLabel",
2048
- "hint": "_367UGW_hint",
2049
- "errorActionLabel": "_367UGW_errorActionLabel"
2169
+ "modeHint": "_367UGW_modeHint",
2170
+ "progressBadgeSection": "_367UGW_progressBadgeSection",
2171
+ "groupList": "_367UGW_groupList",
2172
+ "groupCard": "_367UGW_groupCard",
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
@@ -3804,7 +3931,11 @@ const SYNC_API = {
3804
3931
  githubPoll: "/api/dsh-config-manager/sync/github/poll",
3805
3932
  githubCancel: "/api/dsh-config-manager/sync/github/cancel",
3806
3933
  history: "/api/dsh-config-manager/sync/history",
3807
- apply: "/api/dsh-config-manager/sync/apply",
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",
3808
3939
  rollback: "/api/dsh-config-manager/sync/rollback"
3809
3940
  };
3810
3941
  /** DSH credentials 中的同步 token 引用名(Host 半同值;仅供提示文案使用,值由 Host 读写) */
@@ -3872,13 +4003,33 @@ var SyncApi = class {
3872
4003
  async githubCancel(flowId) {
3873
4004
  return postJson(SYNC_API.githubCancel, { flowId }, this.t);
3874
4005
  }
3875
- /** 同步历史:列出 localSnapshotsDir 下的快照目录(manifest.json: id/createdAt/sectionHashes) */
4006
+ /** 同步历史:列出本地祖先快照 + 自动同步执行记录(按 createdAt 倒序合并)。 */
3876
4007
  async history() {
3877
4008
  return readJson(await fetch(SYNC_API.history), this.t);
3878
4009
  }
3879
- /** 应用自动应用计划:写本地 + backup + (失败时) rollback + enqueue review;返回 ApplyReport */
3880
- async apply(payload) {
3881
- return postJson(SYNC_API.apply, payload, this.t);
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);
3882
4033
  }
3883
4034
  /** 一键回滚:按 restoreId 调用 backup→rollback */
3884
4035
  async rollback(payload) {
@@ -3961,8 +4112,8 @@ function computeSyncButtons(busy, repoUrl, t = zhUiT) {
3961
4112
  return {
3962
4113
  canPush: enabled,
3963
4114
  canPull: enabled,
3964
- pushLabel: busy === "push" ? t("sync.pushing") : t("sync.pushLabel"),
3965
- 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")
3966
4117
  };
3967
4118
  }
3968
4119
  /** 状态行渲染模型:加载 / 未配置 / 就绪(凭据 + 上次同步 + 通道)/ 错误 */
@@ -4080,11 +4231,78 @@ function githubPollMessage(poll, t = zhUiT) {
4080
4231
  default: return "";
4081
4232
  }
4082
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
+ }
4083
4298
  //#endregion
4084
4299
  //#region src/client/sync/history-model.ts
4085
- /** entries 排序(createdAt 倒序)并组装展示字段。 */
4086
- function projectHistoryRows(entries) {
4087
- return [...entries].sort((a, b) => a.createdAt < b.createdAt ? 1 : a.createdAt > b.createdAt ? -1 : 0);
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;
4088
4306
  }
4089
4307
  /** ISO 时间 → 本地可读字符串(短格式) */
4090
4308
  function formatDateTime(iso) {
@@ -4094,17 +4312,62 @@ function formatDateTime(iso) {
4094
4312
  const pad = (n) => String(n).padStart(2, "0");
4095
4313
  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
4096
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
+ }
4097
4359
  //#endregion
4098
4360
  //#region src/client/sync/SyncHistoryView.tsx
4099
4361
  /**
4100
- * 同步历史视图(M6,P2b):列出 localSnapshotsDir 各快照目录的 manifest.json
4101
- * (id + createdAt + sectionHashes);每行显示:快照 ID、时间、分区数、关联待审数(来自 sync-review-queue.json)。
4362
+ * 同步历史视图(方案 A):列出本地祖先快照目录(kind=apply)+ 自动同步执行记录
4363
+ * (kind=autosync)。自动同步行显示时间/方向/状态/跳过冲突/应用分区,点开可看被跳过
4364
+ * 冲突分区明细。
4102
4365
  *
4103
- * 数据获取:通过 Host IPC 端点(与 SyncSettingsView 同模式)。本组件只负责渲染;
4104
- * 数据加载由宿主 API 提供(不在浏览器侧直接读 fs)。
4366
+ * 数据获取:GET /sync/history { entries: SyncHistoryEntry[] }(按 createdAt 倒序合并)。
4367
+ * 纯函数投影在 ./history-model.ts(node --test 可测),本组件只做装配。
4105
4368
  */
4106
4369
  function SyncHistoryView(props) {
4107
- const { api } = props;
4370
+ const { api, t } = props;
4108
4371
  const [loading, setLoading] = (0, react.useState)(true);
4109
4372
  const [error, setError] = (0, react.useState)(null);
4110
4373
  const [entries, setEntries] = (0, react.useState)([]);
@@ -4114,7 +4377,7 @@ function SyncHistoryView(props) {
4114
4377
  try {
4115
4378
  const data = await api.history();
4116
4379
  if (!cancelled) {
4117
- setEntries(data);
4380
+ setEntries(data.entries);
4118
4381
  setLoading(false);
4119
4382
  }
4120
4383
  } catch (err) {
@@ -4128,40 +4391,426 @@ function SyncHistoryView(props) {
4128
4391
  cancelled = true;
4129
4392
  };
4130
4393
  }, [api]);
4131
- const rows = (0, react.useMemo)(() => projectHistoryRows(entries), [entries]);
4132
- if (loading) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Spinner, { label: "加载同步历史…" });
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") });
4133
4402
  if (error) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Card, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4134
4403
  className: "error",
4135
4404
  children: error
4136
4405
  }) });
4137
- if (rows.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Card, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "尚无同步历史" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: "完成首次 push/pull 后这里会显示快照记录。" })] });
4138
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Card, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SectionTitle, { title: `同步历史(${rows.length})` }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
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", {
4139
4408
  className: "sync-history-table",
4140
4409
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
4141
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: "快照 ID" }),
4142
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: "时间" }),
4143
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: "分区" }),
4144
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: "待审" })
4145
- ] }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: rows.map((r) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
4146
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: r.id }) }),
4147
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: formatDateTime(r.createdAt) }),
4148
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: r.sectionCount }),
4149
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: r.reviewCount > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: r.reviewCount }) : 0 })
4150
- ] }, r.id)) })]
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
+ }) })]
4151
4433
  })] });
4152
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
+ }
4153
4488
  //#endregion
4154
- //#region src/client/sync/SyncPullPreviewView.tsx
4489
+ //#region src/client/sync/SyncConfirmView.tsx
4155
4490
  /**
4156
- * 拉取预览页(M5,P2b):消费 SyncApplyPlan,渲染
4157
- * - 自动已应用列表(autoApplied)—— 绿色状态徽章 + sectionId/description;
4158
- * - 待审变更列表(review)—— 每项三个按钮(用本地 / 用远端 / 跳过)+ 局部差异 diff;
4159
- * - 整体 rollback 后「回滚到应用前」按钮(调用 restoreId);
4160
- * - 全部 review 解决 → 触发 onAllResolved 回调(sync 完成)。
4491
+ * 一键同步差异确认视图(方案 A §5 差异确认数据流)。
4161
4492
  *
4162
- * 设计:与 SyncSettingsView 同模式 —— 全部渲染模型来自 ./sync-view.ts 纯函数,
4163
- * 组件只做装配 + 状态;测试在 node --test 跑纯函数部分。
4493
+ * 消费 POST /sync/sync 返回的 items[],渲染逐项确认列表:
4494
+ * - 每项:kindTag + description + severity + 「采用远端」复选框(默认勾选,可取消);
4495
+ * - Conflict 项:内联弹窗(用本地 / 用远端 / 跳过);
4496
+ * - 底部:「确认导入」(apply-items)+「取消」(cancel)。
4497
+ *
4498
+ * 全部渲染模型来自 ./sync-view.ts 纯函数(node 单测覆盖),组件只做装配 + 交互状态。
4164
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
+ }
4165
4814
  //#endregion
4166
4815
  //#region src/client/sync/SyncSettingsView.tsx
4167
4816
  /**
@@ -4173,8 +4822,10 @@ function SyncHistoryView(props) {
4173
4822
  * + gitBin(可选);
4174
4823
  * - 私有仓库强制提示横幅(常驻);
4175
4824
  * - 推送按钮 → SyncPushReport(快照 id / 分区 / 告警);
4176
- * - 拉取按钮 → SyncPullReport.changes 差异摘要(description/kind/severity)+
4177
- * 「预览不执行导入」提示;needsReview 高亮(v1 不做完整导入接线);
4825
+ * - 拉取按钮 → SyncPullReport.changes 差异摘要(description/kind/severity);
4826
+ * - 【方案 A】一键同步主按钮:拉取 → 差异确认会话(SyncConfirmView 逐项确认)→
4827
+ * 确认导入(apply-items)→ 执行结果 + 一键回滚(restoreId);「选择历史快照」下拉;
4828
+ * - 【方案 A】自动同步设置区块:总开关 + 间隔下拉 + 状态(上次运行 / 下次倒计时);
4178
4829
  * - 状态行:凭据配置 + 上次同步时间 + 通道(来自 GET /sync/status,组件挂载时加载)。
4179
4830
  *
4180
4831
  * 全部渲染模型来自 ./sync-view.ts 纯函数(node 单测覆盖),组件只做装配;
@@ -4189,6 +4840,15 @@ const initialGithub = {
4189
4840
  interval: 5,
4190
4841
  error: null
4191
4842
  };
4843
+ const AUTOSYNC_INTERVAL_OPTIONS = [
4844
+ "5m",
4845
+ "15m",
4846
+ "30m",
4847
+ "60m",
4848
+ "6h",
4849
+ "12h",
4850
+ "24h"
4851
+ ];
4192
4852
  const initial = {
4193
4853
  loading: true,
4194
4854
  loadError: null,
@@ -4199,7 +4859,13 @@ const initial = {
4199
4859
  busy: null,
4200
4860
  pushReport: null,
4201
4861
  pullReport: null,
4202
- applyReport: null,
4862
+ confirmSession: null,
4863
+ snapshots: [],
4864
+ selectedSnapshotId: "",
4865
+ autosync: null,
4866
+ autosyncEnabled: false,
4867
+ autosyncInterval: "30m",
4868
+ lastRestoreId: null,
4203
4869
  error: null,
4204
4870
  github: initialGithub
4205
4871
  };
@@ -4219,7 +4885,7 @@ function SyncSettingsView({ api, t }) {
4219
4885
  }));
4220
4886
  /** GitHub 轮询定时器(卸载/取消时清理,防止泄漏与跨流程串扰) */
4221
4887
  const githubPollTimer = (0, react.useRef)(null);
4222
- /** 挂载时读取同步状态(配置回填 + 上次同步时间 + 凭据状态) */
4888
+ /** 挂载时读取同步状态(配置回填 + 上次同步时间 + 凭据状态 + autosync) */
4223
4889
  const loadStatus = async () => {
4224
4890
  patch({
4225
4891
  loading: true,
@@ -4231,8 +4897,14 @@ function SyncSettingsView({ api, t }) {
4231
4897
  loading: false,
4232
4898
  statusInfo: info,
4233
4899
  repoUrl: info.repoUrl ?? "",
4234
- 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
+ } : {}
4235
4906
  });
4907
+ if (info.autosync === void 0) loadAutosync();
4236
4908
  } catch (err) {
4237
4909
  patch({
4238
4910
  loading: false,
@@ -4240,6 +4912,26 @@ function SyncSettingsView({ api, t }) {
4240
4912
  });
4241
4913
  }
4242
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
+ };
4243
4935
  (0, react.useEffect)(() => {
4244
4936
  loadStatus();
4245
4937
  }, []);
@@ -4365,19 +5057,35 @@ function SyncSettingsView({ api, t }) {
4365
5057
  });
4366
5058
  }
4367
5059
  };
4368
- /** P2:自动应用(Host engine.merge + classifyMergePlan + applyMergePlan 走完整链路) */
4369
- const runApply = async () => {
5060
+ /** 一键同步:拉取 差异确认会话(先取消旧会话,再发起新会话)。 */
5061
+ const runSync = async (snapshotId) => {
5062
+ if (state.confirmSession !== null) try {
5063
+ await api.cancel(state.confirmSession.syncSessionId);
5064
+ } catch {}
4370
5065
  patch({
4371
- busy: "apply",
5066
+ busy: "sync",
4372
5067
  error: null,
4373
- applyReport: null
5068
+ confirmSession: null,
5069
+ lastRestoreId: null
4374
5070
  });
4375
5071
  try {
4376
- const report = await api.apply(payload());
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
+ }
4377
5083
  patch({
4378
5084
  busy: null,
4379
- applyReport: report
5085
+ confirmSession: session,
5086
+ token: ""
4380
5087
  });
5088
+ loadSnapshots();
4381
5089
  } catch (err) {
4382
5090
  patch({
4383
5091
  busy: null,
@@ -4385,23 +5093,50 @@ function SyncSettingsView({ api, t }) {
4385
5093
  });
4386
5094
  }
4387
5095
  };
4388
- /** P2:一键回滚(按 apply 返回的 restoreId 调 Host /sync/rollback) */
4389
- const runRollback = async (restoreId) => {
5096
+ /** 用户取消差异确认:清除会话,复位到空闲。 */
5097
+ const cancelConfirm = () => {
5098
+ patch({ confirmSession: null });
5099
+ };
5100
+ /** 从 SyncConfirmView 透传的一键回滚完成信号。 */
5101
+ const onRollbackApplied = () => {
5102
+ patch({ lastRestoreId: null });
5103
+ };
5104
+ const toggleAutosync = async (enabled) => {
4390
5105
  patch({
4391
- busy: "rollback",
5106
+ autosyncEnabled: enabled,
4392
5107
  error: null
4393
5108
  });
4394
5109
  try {
4395
- await api.rollback({ restoreId });
5110
+ const updated = await api.autosyncUpdate({
5111
+ enabled,
5112
+ interval: state.autosyncInterval
5113
+ });
4396
5114
  patch({
4397
- busy: null,
4398
- applyReport: null
5115
+ autosync: updated,
5116
+ autosyncEnabled: updated.enabled,
5117
+ autosyncInterval: updated.interval
4399
5118
  });
4400
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
+ });
4401
5133
  patch({
4402
- busy: null,
4403
- error: err instanceof Error ? err.message : String(err)
5134
+ autosync: updated,
5135
+ autosyncEnabled: updated.enabled,
5136
+ autosyncInterval: updated.interval
4404
5137
  });
5138
+ } catch (err) {
5139
+ patch({ error: err instanceof Error ? err.message : String(err) });
4405
5140
  }
4406
5141
  };
4407
5142
  const status = computeSyncStatus(state.statusInfo, state.loading, state.loadError, uiT);
@@ -4411,6 +5146,8 @@ function SyncSettingsView({ api, t }) {
4411
5146
  const githubView = computeGithubLoginView(state.github.phase, state.github.userCode, state.github.verificationUri, state.github.error, uiT);
4412
5147
  /** GitHub 流程进行中(请求设备码 / 等待授权 / 轮询):禁用 push/pull,避免无凭据操作 */
4413
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;
4414
5151
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4415
5152
  className: config_manager_module_css_default.viewBody,
4416
5153
  children: [
@@ -4469,7 +5206,7 @@ function SyncSettingsView({ api, t }) {
4469
5206
  className: config_manager_module_css_default.input,
4470
5207
  value: state.token,
4471
5208
  autoComplete: "off",
4472
- placeholder: "ghp_…(可选)",
5209
+ placeholder: t("config.tokenPlaceholder"),
4473
5210
  disabled: state.busy !== null,
4474
5211
  onChange: (e) => {
4475
5212
  patch({ token: e.target.value });
@@ -4571,22 +5308,79 @@ function SyncSettingsView({ api, t }) {
4571
5308
  })] }),
4572
5309
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4573
5310
  className: config_manager_module_css_default.actionRow,
4574
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
4575
- variant: "primary",
4576
- disabled: !buttons.canPush || githubBusy,
4577
- onClick: () => {
4578
- runPush();
4579
- },
4580
- children: state.busy === "push" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Spinner, { label: buttons.pushLabel }) : buttons.pushLabel
4581
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
4582
- disabled: !buttons.canPull || githubBusy,
4583
- onClick: () => {
4584
- runPull();
4585
- },
4586
- children: state.busy === "pull" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Spinner, { label: buttons.pullLabel }) : buttons.pullLabel
4587
- })]
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
+ })
4588
5371
  }),
4589
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
+ }),
4590
5384
  pushView !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Card, { children: [
4591
5385
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4592
5386
  className: config_manager_module_css_default.groupLabel,
@@ -4684,67 +5478,82 @@ function SyncSettingsView({ api, t }) {
4684
5478
  children: pullView.previewHint
4685
5479
  })
4686
5480
  ] }),
4687
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4688
- className: config_manager_module_css_default.actionRow,
4689
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
4690
- variant: "primary",
4691
- disabled: state.busy !== null || state.pullReport === null,
4692
- onClick: () => {
4693
- runApply();
4694
- },
4695
- children: state.busy === "apply" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Spinner, { label: "自动应用中…" }) : "自动应用(合并 + 写本地)"
4696
- })
4697
- }),
4698
- state.applyReport !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Card, { children: [
5481
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Card, { children: [
4699
5482
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4700
5483
  className: config_manager_module_css_default.groupLabel,
4701
- children: "自动应用结果"
5484
+ children: t("autosync.title")
4702
5485
  }),
4703
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Banner, {
4704
- kind: state.applyReport.ok ? "ok" : "error",
4705
- children: state.applyReport.ok ? `已应用 ${state.applyReport.applied.length} 个分区(restoreId=${state.applyReport.restoreId})` : `自动应用失败(已整体回滚,restoreId=${state.applyReport.restoreId})`
5486
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5487
+ className: config_manager_module_css_default.hint,
5488
+ children: t("autosync.description")
4706
5489
  }),
4707
- state.applyReport.applied.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4708
- className: config_manager_module_css_default.fieldLabel,
4709
- children: "已写入"
4710
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
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", {
4711
5527
  className: config_manager_module_css_default.statRow,
4712
- children: state.applyReport.applied.map((sid) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
4713
- kind: "ok",
4714
- children: sid
4715
- }, sid))
4716
- })] }),
4717
- state.applyReport.warnings.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4718
- className: config_manager_module_css_default.fieldLabel,
4719
- children: "告警"
4720
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
4721
- className: config_manager_module_css_default.warnList,
4722
- children: state.applyReport.warnings.map((w, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: w }, i))
4723
- })] }),
4724
- state.applyReport.review.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4725
- className: config_manager_module_css_default.fieldLabel,
4726
- children: "待审(已入 sync-review-queue.json)"
4727
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
4728
- className: config_manager_module_css_default.warnList,
4729
- children: state.applyReport.review.map((r, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
4730
- r.sectionId,
4731
- " — ",
4732
- r.description
4733
- ] }, i))
4734
- })] }),
4735
- !state.applyReport.ok && state.applyReport.restoreId !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
4736
- variant: "danger",
4737
- disabled: state.busy !== null,
4738
- onClick: () => {
4739
- runRollback(state.applyReport.restoreId);
4740
- },
4741
- children: state.busy === "rollback" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Spinner, { label: "回滚中…" }) : "回滚到应用前"
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
+ })]
4742
5535
  })
4743
5536
  ] }),
4744
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SyncHistoryView, { api })
5537
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SyncHistoryView, {
5538
+ api,
5539
+ t
5540
+ })
4745
5541
  ]
4746
5542
  });
4747
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
+ }
4748
5557
  //#endregion
4749
5558
  //#region src/client/ConfigManagerSection.tsx
4750
5559
  /**
@@ -5101,6 +5910,7 @@ const zh = {
5101
5910
  "config.token": "认证 token",
5102
5911
  "config.tokenHint": "将安全写入 DSH credentials(引用名 {ref}),不会写入同步文件或日志。留空表示沿用已保存的凭据。",
5103
5912
  "config.tokenSaved": "凭据已配置",
5913
+ "config.tokenPlaceholder": "ghp_…(可选)",
5104
5914
  "config.gitBin": "git 可执行文件(可选)",
5105
5915
  "config.gitBinHint": "缺省使用 PATH 中的 git。",
5106
5916
  "github.title": "GitHub 登录",
@@ -5125,6 +5935,84 @@ const zh = {
5125
5935
  "change.total": "共 {total} 项变更",
5126
5936
  "sections.title": "同步分区",
5127
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": "自动同步",
5128
6016
  "common.close": "关闭",
5129
6017
  "common.retry": "重试",
5130
6018
  "common.loading": "加载中…"
@@ -5139,6 +6027,7 @@ const en = {
5139
6027
  "config.token": "Auth token",
5140
6028
  "config.tokenHint": "Securely written into DSH credentials (ref {ref}); never written into sync files or logs. Leave blank to reuse the saved credential.",
5141
6029
  "config.tokenSaved": "Credential configured",
6030
+ "config.tokenPlaceholder": "ghp_… (optional)",
5142
6031
  "config.gitBin": "git executable (optional)",
5143
6032
  "config.gitBinHint": "Defaults to git on PATH.",
5144
6033
  "github.title": "GitHub Sign-in",
@@ -5163,6 +6052,84 @@ const en = {
5163
6052
  "change.total": "{total} change(s)",
5164
6053
  "sections.title": "Synced Sections",
5165
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",
5166
6133
  "common.close": "Close",
5167
6134
  "common.retry": "Retry",
5168
6135
  "common.loading": "Loading…"