dsh-config-manager 0.1.47 → 0.1.49

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 (39) hide show
  1. package/README.md +15 -0
  2. package/README.zh-CN.md +15 -0
  3. package/lib/client.d.ts +70 -3
  4. package/lib/client.js +792 -268
  5. package/lib/client.js.map +1 -1
  6. package/lib/core/restore.d.ts +7 -0
  7. package/lib/core/restore.js +4 -0
  8. package/lib/core/restore.js.map +1 -1
  9. package/lib/core/run-registry.d.ts +11 -4
  10. package/lib/core/run-registry.js +14 -7
  11. package/lib/core/run-registry.js.map +1 -1
  12. package/lib/index.d.ts +7 -1
  13. package/lib/index.js +131 -19
  14. package/lib/index.js.map +1 -1
  15. package/lib/ui/backup-schedule.d.ts +49 -0
  16. package/lib/ui/backup-schedule.js +42 -0
  17. package/lib/ui/backup-schedule.js.map +1 -0
  18. package/package.json +1 -1
  19. package/src/client/api.ts +35 -1
  20. package/src/client/common/ConfirmDialog.tsx +4 -2
  21. package/src/client/common/ErrorBanner.tsx +8 -4
  22. package/src/client/common/ReportView.tsx +10 -3
  23. package/src/client/common/ui.tsx +9 -2
  24. package/src/client/config-manager.module.css +22 -0
  25. package/src/client/export/ExportView.tsx +15 -5
  26. package/src/client/import/ImportWizardView.tsx +75 -14
  27. package/src/client/locales.ts +46 -0
  28. package/src/client/market/MarketPanel.tsx +9 -4
  29. package/src/client/market/MyConfigsView.tsx +8 -8
  30. package/src/client/run-store.test.ts +112 -1
  31. package/src/client/run-store.ts +78 -4
  32. package/src/client/snapshots/SnapshotsPanel.tsx +261 -7
  33. package/src/client/sync/SyncHistoryView.tsx +16 -2
  34. package/src/core/restore.ts +7 -0
  35. package/src/core/run-registry.test.ts +40 -0
  36. package/src/core/run-registry.ts +16 -9
  37. package/src/index.ts +129 -18
  38. package/src/ui/backup-schedule.test.ts +53 -0
  39. package/src/ui/backup-schedule.ts +83 -0
package/lib/client.js CHANGED
@@ -539,6 +539,8 @@ const CONFIG_MANAGER_API = {
539
539
  runs: "/api/dsh-config-manager/runs",
540
540
  snapshots: "/api/dsh-config-manager/snapshots",
541
541
  restore: "/api/dsh-config-manager/restore",
542
+ backupSchedule: "/api/dsh-config-manager/backup-schedule",
543
+ backupScheduleRun: "/api/dsh-config-manager/backup-schedule/run",
542
544
  starPrompt: "/api/dsh-config-manager/star-prompt"
543
545
  };
544
546
  /** 导出请求超时(ms):与 Host 半 ROUTE_TIMEOUT_MS 对齐,防止宿主卡死时 UI 无限等待 */
@@ -784,6 +786,26 @@ var ConfigManagerApi = class {
784
786
  })
785
787
  }), this.t);
786
788
  }
789
+ /** 读取定时备份配置(enabled / interval / 上次运行状态;无敏感字段)。 */
790
+ async backupSchedule() {
791
+ return (await readJson$3(await fetch(CONFIG_MANAGER_API.backupSchedule), this.t)).schedule;
792
+ }
793
+ /** 保存定时备份设置(enabled + interval);Host 校验后原子写 + 重排调度器。 */
794
+ async saveBackupSchedule(draft) {
795
+ return (await readJson$3(await fetch(CONFIG_MANAGER_API.backupSchedule, {
796
+ method: "PUT",
797
+ headers: { "content-type": "application/json" },
798
+ body: JSON.stringify(draft)
799
+ }), this.t)).schedule;
800
+ }
801
+ /** 立即执行一次全量备份(复用调度器 runOnce,防重;返回执行结果 + 最新配置)。 */
802
+ async runBackupNow() {
803
+ return readJson$3(await fetch(CONFIG_MANAGER_API.backupScheduleRun, {
804
+ method: "POST",
805
+ headers: { "content-type": "application/json" },
806
+ body: JSON.stringify({})
807
+ }), this.t);
808
+ }
787
809
  };
788
810
  //#endregion
789
811
  //#region src/ui/conflict-view.ts
@@ -2300,9 +2322,11 @@ function defaultSnapshotsState() {
2300
2322
  return {
2301
2323
  selectedId: null,
2302
2324
  plan: null,
2325
+ running: false,
2303
2326
  report: null,
2304
2327
  actionError: null,
2305
- error: null
2328
+ error: null,
2329
+ backupDraft: null
2306
2330
  };
2307
2331
  }
2308
2332
  function defaultState() {
@@ -2379,9 +2403,11 @@ function toSnapshotsStoreSlice(s) {
2379
2403
  return {
2380
2404
  selectedId: s.selectedId,
2381
2405
  plan: s.plan,
2406
+ running: s.running,
2382
2407
  report: s.report,
2383
2408
  actionError: s.actionError,
2384
- error: s.error
2409
+ error: s.error,
2410
+ backupDraft: s.backupDraft
2385
2411
  };
2386
2412
  }
2387
2413
  /**
@@ -2416,7 +2442,10 @@ function toPersistedState(state) {
2416
2442
  }
2417
2443
  },
2418
2444
  market: state.market,
2419
- snapshots: state.snapshots
2445
+ snapshots: {
2446
+ ...state.snapshots,
2447
+ running: false
2448
+ }
2420
2449
  };
2421
2450
  }
2422
2451
  /** 解析 + 轻量校验持久化状态;损坏/版本不符返回 null(调用方回退默认并清键)。 */
@@ -2701,7 +2730,8 @@ var RunStore = class {
2701
2730
  },
2702
2731
  snapshots: {
2703
2732
  ...defaultSnapshotsState(),
2704
- ...parsed.snapshots
2733
+ ...parsed.snapshots,
2734
+ running: false
2705
2735
  }
2706
2736
  };
2707
2737
  if (this.state.import.containerEncrypted && this.state.import.phase !== "preview" && this.state.import.phase !== "decrypt-archive") this.state.import.phase = "decrypt-archive";
@@ -2799,6 +2829,18 @@ var RunStore = class {
2799
2829
  error: "上次导入任务已结束或超过保留期,结果无法恢复,请重新导入"
2800
2830
  } });
2801
2831
  }
2832
+ const restoreRun = active.find((r) => r.kind === "restore");
2833
+ if (restoreRun !== void 0) {
2834
+ resumed = true;
2835
+ this.patch({ snapshots: {
2836
+ running: true,
2837
+ actionError: null
2838
+ } });
2839
+ this.pollRun(restoreRun.runId, "restore");
2840
+ } else if (this.state.snapshots.running) this.patch({ snapshots: {
2841
+ running: false,
2842
+ actionError: "上次恢复任务已结束但结果无法恢复,请重新执行"
2843
+ } });
2802
2844
  return resumed;
2803
2845
  }
2804
2846
  /** 停止全部轮询(视图卸载时调用;服务端执行不受影响,重开面板再 resume)。 */
@@ -2886,7 +2928,11 @@ var RunStore = class {
2886
2928
  patchProgress(kind, state) {
2887
2929
  const event = mapRunProgress(kind, state);
2888
2930
  if (kind === "export") this.patch({ export: { progress: event } });
2889
- else this.patch({ import: { progress: event } });
2931
+ else if (kind === "restore") this.patch({ snapshots: { running: true } });
2932
+ else this.patch({ import: {
2933
+ progress: event,
2934
+ runId: state.runId
2935
+ } });
2890
2936
  }
2891
2937
  /** run 完成/失败:把 RunState.result 回填 store(并镜像回控制器)。 */
2892
2938
  applySettled(kind, state) {
@@ -2914,6 +2960,19 @@ var RunStore = class {
2914
2960
  } });
2915
2961
  return;
2916
2962
  }
2963
+ if (kind === "restore") {
2964
+ const result = state.result;
2965
+ if (state.status === "done" && result !== void 0 && typeof result === "object") this.patch({ snapshots: {
2966
+ running: false,
2967
+ report: result,
2968
+ actionError: null
2969
+ } });
2970
+ else this.patch({ snapshots: {
2971
+ running: false,
2972
+ actionError: state.error ?? "恢复失败(结果不可用)"
2973
+ } });
2974
+ return;
2975
+ }
2917
2976
  const result = state.result;
2918
2977
  if (state.status === "done" && result !== void 0 && typeof result === "object") {
2919
2978
  const wizard = this.importWizardInst;
@@ -2959,6 +3018,10 @@ var RunStore = class {
2959
3018
  runId: null,
2960
3019
  error: "任务已结束或超过保留期,进度不可恢复"
2961
3020
  } });
3021
+ else if (kind === "restore") this.patch({ snapshots: {
3022
+ running: false,
3023
+ actionError: "恢复任务已结束或超过保留期,进度不可恢复"
3024
+ } });
2962
3025
  else {
2963
3026
  this.importWizardInst?.reset();
2964
3027
  this.patch({ import: {
@@ -2979,7 +3042,7 @@ var RunStore = class {
2979
3042
  const runStore = new RunStore();
2980
3043
  //#endregion
2981
3044
  //#region \0config-manager-css:/home/runner/work/dsh-config-manager/dsh-config-manager/src/client/config-manager.module.css.js
2982
- 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_wizardCard{gap:14px}._367UGW_wizardCard ._367UGW_field{gap:8px}._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_select{-webkit-appearance:none;appearance:none;color:var(--dsw-alias-label-primary);background-color:var(--dsw-specific-input-major);background-image:linear-gradient(45deg, transparent 50%, var(--dsw-alias-label-secondary) 50%), linear-gradient(135deg, var(--dsw-alias-label-secondary) 50%, transparent 50%);border:1px solid var(--dsw-alias-border-l2);cursor:pointer;background-position:calc(100% - 17px) calc(50% - 1px),calc(100% - 12px) calc(50% - 1px);background-repeat:no-repeat;background-size:5px 5px,5px 5px;border-radius:8px;outline:none;max-width:100%;padding:7px 30px 7px 10px;font-family:inherit;font-size:13px}._367UGW_select:hover:not(:disabled){border-color:var(--dsw-alias-border-l1)}._367UGW_select:focus{border-color:var(--dsw-alias-state-business-primary)}._367UGW_select:disabled{opacity:.55;cursor:default}._367UGW_select option{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2)}._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_optionsHeader{color:var(--dsw-alias-label-primary);letter-spacing:.02em;margin-bottom:2px;font-size:12px;font-weight:600}._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}._367UGW_logPanel{flex-direction:column;flex:none;gap:6px;display:flex}._367UGW_logHeader{color:var(--dsw-alias-label-secondary);font-size:12.5px;font-weight:600}._367UGW_logScroll{overscroll-behavior:contain;background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;max-height:200px;padding:2px 10px 6px;overflow-y:auto}._367UGW_logLine{white-space:pre-wrap;word-break:break-all;color:var(--dsw-alias-label-secondary);padding:1px 0;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px;line-height:1.6}._367UGW_logEmpty{color:var(--dsw-alias-label-tertiary);padding:6px 0;font-size:11.5px}@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_confirmScroll{overscroll-behavior:contain;background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;max-height:280px;padding:2px 10px 6px;overflow-y:auto}._367UGW_confirmScroll ._367UGW_reportList{margin-top:4px;margin-bottom:0}._367UGW_pullScroll{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 6px;overflow-y:auto}._367UGW_pullScroll ._367UGW_reportList{margin-top:4px;margin-bottom:0}._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}._367UGW_aboutLinkRow{flex-wrap:wrap;align-items:center;gap:10px;display:flex}._367UGW_aboutAuthor{color:var(--dsw-alias-label-primary);cursor:pointer;border-radius:6px;align-items:center;gap:6px;padding:4px 8px;font-size:13px;text-decoration:none;display:inline-flex}._367UGW_aboutAuthor:hover{background:var(--dsw-alias-interactive-bg-hover)}._367UGW_dialogMask{z-index:100;background:color-mix(in srgb, var(--dsw-alias-bg-base) 55%, transparent);justify-content:center;align-items:center;padding:20px;display:flex;position:fixed;inset:0}._367UGW_dialogCard{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);width:min(420px,100%);max-height:80vh;box-shadow:0 8px 24px color-mix(in srgb, var(--dsw-alias-bg-base) 40%, transparent);border-radius:10px;flex-direction:column;gap:10px;padding:14px;display:flex}._367UGW_dialogHeader{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:600}._367UGW_dialogBody{overscroll-behavior:contain;max-height:240px;color:var(--dsw-alias-label-primary);white-space:pre-wrap;flex-direction:column;gap:10px;font-size:13px;line-height:1.6;display:flex;overflow-y:auto}._367UGW_dialogWide{width:min(560px,100%)}._367UGW_dialogBodyScroll{overscroll-behavior:contain;max-height:70vh;color:var(--dsw-alias-label-primary);white-space:normal;flex-direction:column;gap:10px;font-size:13px;line-height:1.6;display:flex;overflow-y:auto}._367UGW_dialogHeaderRow{justify-content:space-between;align-items:center;gap:10px;display:flex}._367UGW_dialogClose{color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:6px;padding:2px 8px;font-size:16px;line-height:1}._367UGW_dialogClose:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-hover)}";
3045
+ 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_wizardCard{gap:14px}._367UGW_wizardCard ._367UGW_field{gap:8px}._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_select{-webkit-appearance:none;appearance:none;color:var(--dsw-alias-label-primary);background-color:var(--dsw-specific-input-major);background-image:linear-gradient(45deg, transparent 50%, var(--dsw-alias-label-secondary) 50%), linear-gradient(135deg, var(--dsw-alias-label-secondary) 50%, transparent 50%);border:1px solid var(--dsw-alias-border-l2);cursor:pointer;background-position:calc(100% - 17px) calc(50% - 1px),calc(100% - 12px) calc(50% - 1px);background-repeat:no-repeat;background-size:5px 5px,5px 5px;border-radius:8px;outline:none;max-width:100%;padding:7px 30px 7px 10px;font-family:inherit;font-size:13px}._367UGW_select:hover:not(:disabled){border-color:var(--dsw-alias-border-l1)}._367UGW_select:focus{border-color:var(--dsw-alias-state-business-primary)}._367UGW_select:disabled{opacity:.55;cursor:default}._367UGW_select option{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2)}._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_optionsHeader{color:var(--dsw-alias-label-primary);letter-spacing:.02em;margin-bottom:2px;font-size:12px;font-weight:600}._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}._367UGW_logPanel{flex-direction:column;flex:none;gap:6px;display:flex}._367UGW_logHeader{color:var(--dsw-alias-label-secondary);align-items:center;gap:8px;font-size:12.5px;font-weight:600;display:flex}._367UGW_logJumpButton{font-family:var(--dsw-font-family);color:var(--dsw-alias-label-secondary);border:1px solid var(--dsw-alias-border-l2);cursor:pointer;background:0 0;border-radius:999px;padding:3px 8px;font-size:11px;line-height:1}._367UGW_logJumpButton:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}._367UGW_logScroll{overscroll-behavior:contain;background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;max-height:200px;padding:2px 10px 6px;overflow-y:auto}._367UGW_logLine{white-space:pre-wrap;word-break:break-all;color:var(--dsw-alias-label-secondary);padding:1px 0;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px;line-height:1.6}._367UGW_logEmpty{color:var(--dsw-alias-label-tertiary);padding:6px 0;font-size:11.5px}@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_confirmScroll{overscroll-behavior:contain;background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;max-height:280px;padding:2px 10px 6px;overflow-y:auto}._367UGW_confirmScroll ._367UGW_reportList{margin-top:4px;margin-bottom:0}._367UGW_pullScroll{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 6px;overflow-y:auto}._367UGW_pullScroll ._367UGW_reportList{margin-top:4px;margin-bottom:0}._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}._367UGW_aboutLinkRow{flex-wrap:wrap;align-items:center;gap:10px;display:flex}._367UGW_aboutAuthor{color:var(--dsw-alias-label-primary);cursor:pointer;border-radius:6px;align-items:center;gap:6px;padding:4px 8px;font-size:13px;text-decoration:none;display:inline-flex}._367UGW_aboutAuthor:hover{background:var(--dsw-alias-interactive-bg-hover)}._367UGW_dialogMask{z-index:100;background:color-mix(in srgb, var(--dsw-alias-bg-base) 55%, transparent);justify-content:center;align-items:center;padding:20px;display:flex;position:fixed;inset:0}._367UGW_dialogCard{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);width:min(420px,100%);max-height:80vh;box-shadow:0 8px 24px color-mix(in srgb, var(--dsw-alias-bg-base) 40%, transparent);border-radius:10px;flex-direction:column;gap:10px;padding:14px;display:flex}._367UGW_dialogHeader{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:600}._367UGW_dialogBody{overscroll-behavior:contain;max-height:240px;color:var(--dsw-alias-label-primary);white-space:pre-wrap;flex-direction:column;gap:10px;font-size:13px;line-height:1.6;display:flex;overflow-y:auto}._367UGW_dialogWide{width:min(560px,100%)}._367UGW_dialogBodyScroll{overscroll-behavior:contain;max-height:70vh;color:var(--dsw-alias-label-primary);white-space:normal;flex-direction:column;gap:10px;font-size:13px;line-height:1.6;display:flex;overflow-y:auto}._367UGW_dialogHeaderRow{justify-content:space-between;align-items:center;gap:10px;display:flex}._367UGW_dialogClose{color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:6px;padding:2px 8px;font-size:16px;line-height:1}._367UGW_dialogClose:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-hover)}";
2983
3046
  const tagId = "dsh-config-manager/config-manager.module.css";
2984
3047
  if (typeof document !== "undefined" && document.querySelector("style[data-tag=\"dsh-config-manager/config-manager.module.css\"]") === null) {
2985
3048
  const tag = document.createElement("style");
@@ -2989,125 +3052,126 @@ if (typeof document !== "undefined" && document.querySelector("style[data-tag=\"
2989
3052
  document.head.appendChild(tag);
2990
3053
  }
2991
3054
  var config_manager_module_css_default = {
2992
- "checkboxRow": "_367UGW_checkboxRow",
2993
- "statRow": "_367UGW_statRow",
2994
- "progressBar": "_367UGW_progressBar",
3055
+ "progressLabel": "_367UGW_progressLabel",
3056
+ "sectionHeader": "_367UGW_sectionHeader",
3057
+ "dialogMask": "_367UGW_dialogMask",
3058
+ "secretFields": "_367UGW_secretFields",
3059
+ "badgeInfo": "_367UGW_badgeInfo",
3060
+ "groupList": "_367UGW_groupList",
3061
+ "progressMeta": "_367UGW_progressMeta",
3062
+ "snapshotRowHeader": "_367UGW_snapshotRowHeader",
3063
+ "banner": "_367UGW_banner",
3064
+ "reportList": "_367UGW_reportList",
3065
+ "optionsCard": "_367UGW_optionsCard",
2995
3066
  "reportView": "_367UGW_reportView",
2996
- "badgeError": "_367UGW_badgeError",
2997
- "fieldLabel": "_367UGW_fieldLabel",
2998
3067
  "reportScroll": "_367UGW_reportScroll",
2999
- "pathOld": "_367UGW_pathOld",
3000
- "progressDetail": "_367UGW_progressDetail",
3001
- "warnText": "_367UGW_warnText",
3002
- "progressTrack": "_367UGW_progressTrack",
3003
- "progressIndeterminate": "_367UGW_progressIndeterminate",
3068
+ "errorItem": "_367UGW_errorItem",
3069
+ "errorFooter": "_367UGW_errorFooter",
3070
+ "errorTitle": "_367UGW_errorTitle",
3004
3071
  "rowActions": "_367UGW_rowActions",
3005
- "conflictOptions": "_367UGW_conflictOptions",
3006
- "sectionSubtitle": "_367UGW_sectionSubtitle",
3007
- "select": "_367UGW_select",
3008
- "optionsCard": "_367UGW_optionsCard",
3009
- "groupLabel": "_367UGW_groupLabel",
3010
- "progressBadge": "_367UGW_progressBadge",
3011
- "reportText": "_367UGW_reportText",
3012
- "errorBanner": "_367UGW_errorBanner",
3013
- "pathNew": "_367UGW_pathNew",
3014
- "logLine": "_367UGW_logLine",
3015
- "pathMappingList": "_367UGW_pathMappingList",
3016
- "progressPercent": "_367UGW_progressPercent",
3072
+ "actionRow": "_367UGW_actionRow",
3073
+ "spinnerWrap": "_367UGW_spinnerWrap",
3074
+ "badgeWarn": "_367UGW_badgeWarn",
3075
+ "errorList": "_367UGW_errorList",
3076
+ "groupCard": "_367UGW_groupCard",
3077
+ "progressIndeterminate": "_367UGW_progressIndeterminate",
3078
+ "groupNote": "_367UGW_groupNote",
3079
+ "pathOld": "_367UGW_pathOld",
3017
3080
  "hint": "_367UGW_hint",
3018
- "modeTabs": "_367UGW_modeTabs",
3081
+ "modeHint": "_367UGW_modeHint",
3082
+ "pathMappingList": "_367UGW_pathMappingList",
3083
+ "progressBar": "_367UGW_progressBar",
3019
3084
  "reportFooter": "_367UGW_reportFooter",
3020
- "planScroll": "_367UGW_planScroll",
3021
- "dialogMask": "_367UGW_dialogMask",
3022
- "errorAction": "_367UGW_errorAction",
3023
- "dialogClose": "_367UGW_dialogClose",
3024
- "sectionTitleBlock": "_367UGW_sectionTitleBlock",
3025
- "severityError": "_367UGW_severityError",
3026
- "spinnerWrap": "_367UGW_spinnerWrap",
3027
- "diffScroll": "_367UGW_diffScroll",
3028
- "categoryDesc": "_367UGW_categoryDesc",
3029
- "kindTag": "_367UGW_kindTag",
3030
- "spinnerLabel": "_367UGW_spinnerLabel",
3031
- "warnList": "_367UGW_warnList",
3032
- "progressBadgeCount": "_367UGW_progressBadgeCount",
3085
+ "spinner": "_367UGW_spinner",
3086
+ "dshCmSpin": "_367UGW_dshCmSpin",
3087
+ "secretsList": "_367UGW_secretsList",
3088
+ "logLine": "_367UGW_logLine",
3089
+ "conflictHead": "_367UGW_conflictHead",
3090
+ "ghostButton": "_367UGW_ghostButton",
3091
+ "sectionTitle": "_367UGW_sectionTitle",
3033
3092
  "empty": "_367UGW_empty",
3034
- "confirmScroll": "_367UGW_confirmScroll",
3035
- "pullScroll": "_367UGW_pullScroll",
3036
- "errorItem": "_367UGW_errorItem",
3093
+ "dialogWide": "_367UGW_dialogWide",
3094
+ "logPanel": "_367UGW_logPanel",
3037
3095
  "dialogHeader": "_367UGW_dialogHeader",
3038
- "optionsHeader": "_367UGW_optionsHeader",
3039
- "conflictDetail": "_367UGW_conflictDetail",
3040
- "reportList": "_367UGW_reportList",
3041
- "errorTitle": "_367UGW_errorTitle",
3042
- "progressBlock": "_367UGW_progressBlock",
3043
- "pathRow": "_367UGW_pathRow",
3044
- "hiddenFile": "_367UGW_hiddenFile",
3045
- "errorLine": "_367UGW_errorLine",
3046
- "groupNote": "_367UGW_groupNote",
3047
- "formError": "_367UGW_formError",
3048
- "snapshotRowHeader": "_367UGW_snapshotRowHeader",
3049
- "progressLabel": "_367UGW_progressLabel",
3050
- "aboutAuthor": "_367UGW_aboutAuthor",
3051
- "ghostButton": "_367UGW_ghostButton",
3052
3096
  "badge": "_367UGW_badge",
3097
+ "pathRow": "_367UGW_pathRow",
3098
+ "aboutLinkRow": "_367UGW_aboutLinkRow",
3099
+ "snapshotList": "_367UGW_snapshotList",
3100
+ "modeTab": "_367UGW_modeTab",
3101
+ "logScroll": "_367UGW_logScroll",
3053
3102
  "groupHeader": "_367UGW_groupHeader",
3103
+ "progressBadge": "_367UGW_progressBadge",
3104
+ "modeTabs": "_367UGW_modeTabs",
3105
+ "dialogBody": "_367UGW_dialogBody",
3106
+ "statRow": "_367UGW_statRow",
3107
+ "field": "_367UGW_field",
3108
+ "dialogBodyScroll": "_367UGW_dialogBodyScroll",
3109
+ "radioLabel": "_367UGW_radioLabel",
3110
+ "progressBadgeSection": "_367UGW_progressBadgeSection",
3111
+ "progressPercent": "_367UGW_progressPercent",
3112
+ "dangerButton": "_367UGW_dangerButton",
3054
3113
  "viewTab": "_367UGW_viewTab",
3055
- "modeHint": "_367UGW_modeHint",
3056
- "badgeWarn": "_367UGW_badgeWarn",
3114
+ "categoryItem": "_367UGW_categoryItem",
3057
3115
  "conflictId": "_367UGW_conflictId",
3058
- "dialogBodyScroll": "_367UGW_dialogBodyScroll",
3116
+ "groupItems": "_367UGW_groupItems",
3117
+ "viewBody": "_367UGW_viewBody",
3118
+ "dialogClose": "_367UGW_dialogClose",
3119
+ "dialogCard": "_367UGW_dialogCard",
3120
+ "snapshotRow": "_367UGW_snapshotRow",
3121
+ "conflictOptions": "_367UGW_conflictOptions",
3059
3122
  "dshCmIndeterminate": "_367UGW_dshCmIndeterminate",
3060
- "errorActionLabel": "_367UGW_errorActionLabel",
3123
+ "optionsHeader": "_367UGW_optionsHeader",
3124
+ "fieldLabel": "_367UGW_fieldLabel",
3125
+ "formError": "_367UGW_formError",
3126
+ "logEmpty": "_367UGW_logEmpty",
3127
+ "errorBanner": "_367UGW_errorBanner",
3128
+ "conflictDetail": "_367UGW_conflictDetail",
3061
3129
  "conflictList": "_367UGW_conflictList",
3062
- "groupList": "_367UGW_groupList",
3063
- "sectionTitle": "_367UGW_sectionTitle",
3064
- "progressBarDone": "_367UGW_progressBarDone",
3065
- "snapshotRow": "_367UGW_snapshotRow",
3066
- "progressBadgeSection": "_367UGW_progressBadgeSection",
3067
- "logPanel": "_367UGW_logPanel",
3068
- "wizardCard": "_367UGW_wizardCard",
3069
- "dialogHeaderRow": "_367UGW_dialogHeaderRow",
3070
- "spinner": "_367UGW_spinner",
3071
- "groupCard": "_367UGW_groupCard",
3072
- "conflictHead": "_367UGW_conflictHead",
3073
- "categoryName": "_367UGW_categoryName",
3074
3130
  "pathValue": "_367UGW_pathValue",
3075
- "dialogCard": "_367UGW_dialogCard",
3076
- "logEmpty": "_367UGW_logEmpty",
3077
- "actionRow": "_367UGW_actionRow",
3078
- "snapshotList": "_367UGW_snapshotList",
3079
- "dialogWide": "_367UGW_dialogWide",
3080
3131
  "errorReason": "_367UGW_errorReason",
3081
- "secretFields": "_367UGW_secretFields",
3082
- "secretsList": "_367UGW_secretsList",
3083
3132
  "badgeOk": "_367UGW_badgeOk",
3084
- "groupItems": "_367UGW_groupItems",
3085
- "categoryItem": "_367UGW_categoryItem",
3086
- "logHeader": "_367UGW_logHeader",
3133
+ "wizardCard": "_367UGW_wizardCard",
3134
+ "groupLabel": "_367UGW_groupLabel",
3135
+ "reportText": "_367UGW_reportText",
3136
+ "sectionSubtitle": "_367UGW_sectionSubtitle",
3137
+ "dialogHeaderRow": "_367UGW_dialogHeaderRow",
3138
+ "card": "_367UGW_card",
3139
+ "viewTabs": "_367UGW_viewTabs",
3140
+ "categoryName": "_367UGW_categoryName",
3141
+ "kindTag": "_367UGW_kindTag",
3142
+ "errorAction": "_367UGW_errorAction",
3143
+ "badgeError": "_367UGW_badgeError",
3144
+ "progressBarDone": "_367UGW_progressBarDone",
3145
+ "sectionBody": "_367UGW_sectionBody",
3146
+ "progressTrack": "_367UGW_progressTrack",
3147
+ "logJumpButton": "_367UGW_logJumpButton",
3148
+ "categoryDesc": "_367UGW_categoryDesc",
3149
+ "checkboxRow": "_367UGW_checkboxRow",
3150
+ "progressDetail": "_367UGW_progressDetail",
3151
+ "primaryButton": "_367UGW_primaryButton",
3152
+ "errorActionLabel": "_367UGW_errorActionLabel",
3087
3153
  "input": "_367UGW_input",
3154
+ "warnText": "_367UGW_warnText",
3155
+ "logHeader": "_367UGW_logHeader",
3156
+ "progressBlock": "_367UGW_progressBlock",
3157
+ "confirmScroll": "_367UGW_confirmScroll",
3158
+ "sectionTitleBlock": "_367UGW_sectionTitleBlock",
3159
+ "aboutAuthor": "_367UGW_aboutAuthor",
3160
+ "section": "_367UGW_section",
3161
+ "errorLine": "_367UGW_errorLine",
3162
+ "hiddenFile": "_367UGW_hiddenFile",
3163
+ "warnList": "_367UGW_warnList",
3164
+ "rollbackBox": "_367UGW_rollbackBox",
3165
+ "pullScroll": "_367UGW_pullScroll",
3166
+ "diffScroll": "_367UGW_diffScroll",
3088
3167
  "pathIssueKind": "_367UGW_pathIssueKind",
3089
- "aboutLinkRow": "_367UGW_aboutLinkRow",
3090
- "viewBody": "_367UGW_viewBody",
3091
- "modeTab": "_367UGW_modeTab",
3092
- "badgeInfo": "_367UGW_badgeInfo",
3093
- "errorList": "_367UGW_errorList",
3094
- "dshCmSpin": "_367UGW_dshCmSpin",
3095
- "viewTabs": "_367UGW_viewTabs",
3168
+ "spinnerLabel": "_367UGW_spinnerLabel",
3096
3169
  "conflictItem": "_367UGW_conflictItem",
3097
- "rollbackBox": "_367UGW_rollbackBox",
3098
- "dialogBody": "_367UGW_dialogBody",
3099
- "card": "_367UGW_card",
3100
- "sectionHeader": "_367UGW_sectionHeader",
3101
- "progressMeta": "_367UGW_progressMeta",
3102
- "errorFooter": "_367UGW_errorFooter",
3103
- "banner": "_367UGW_banner",
3104
- "field": "_367UGW_field",
3105
- "logScroll": "_367UGW_logScroll",
3106
- "dangerButton": "_367UGW_dangerButton",
3107
- "primaryButton": "_367UGW_primaryButton",
3108
- "radioLabel": "_367UGW_radioLabel",
3109
- "sectionBody": "_367UGW_sectionBody",
3110
- "section": "_367UGW_section"
3170
+ "severityError": "_367UGW_severityError",
3171
+ "pathNew": "_367UGW_pathNew",
3172
+ "select": "_367UGW_select",
3173
+ "planScroll": "_367UGW_planScroll",
3174
+ "progressBadgeCount": "_367UGW_progressBadgeCount"
3111
3175
  };
3112
3176
  //#endregion
3113
3177
  //#region src/client/common/ui.tsx
@@ -3115,15 +3179,19 @@ var config_manager_module_css_default = {
3115
3179
  * 统一按钮(primary=主操作 / ghost=次操作 / danger=危险操作)。
3116
3180
  * 带 href 时渲染同款按钮类的外链 <a>(target=_blank + rel=noreferrer),
3117
3181
  * 外观与普通按钮一致,不破坏既有 <button> 调用。
3182
+ * loading=true 时自动禁用(防重复点击)并标注 aria-busy(无障碍);
3183
+ * 视觉 loading(Spinner)由调用方按既有模式放在 children 中。
3118
3184
  */
3119
- function Button({ variant = "ghost", disabled, onClick, children, title, className, href, newTab = true }) {
3185
+ function Button({ variant = "ghost", disabled, loading = false, onClick, children, title, className, href, newTab = true }) {
3120
3186
  const cls = variant === "primary" ? config_manager_module_css_default.primaryButton : variant === "danger" ? config_manager_module_css_default.dangerButton : config_manager_module_css_default.ghostButton;
3187
+ const effectiveDisabled = disabled === true || loading;
3121
3188
  if (href !== void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
3122
3189
  className: className !== void 0 ? `${cls} ${className}` : cls,
3123
3190
  href,
3124
3191
  target: newTab ? "_blank" : void 0,
3125
3192
  rel: newTab ? "noreferrer" : void 0,
3126
3193
  title,
3194
+ "aria-busy": loading || void 0,
3127
3195
  onClick,
3128
3196
  style: { textDecoration: "none" },
3129
3197
  children
@@ -3131,8 +3199,9 @@ function Button({ variant = "ghost", disabled, onClick, children, title, classNa
3131
3199
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3132
3200
  type: "button",
3133
3201
  className: className !== void 0 ? `${cls} ${className}` : cls,
3134
- disabled,
3202
+ disabled: effectiveDisabled,
3135
3203
  title,
3204
+ "aria-busy": loading || void 0,
3136
3205
  onClick,
3137
3206
  children
3138
3207
  });
@@ -3236,8 +3305,9 @@ function Checkbox({ checked, onChange, label, disabled }) {
3236
3305
  /**
3237
3306
  * 错误横幅:toActionableError 解析为标题 + 原因 + 建议动作,
3238
3307
  * 文本在渲染前再经 redact() 兜底(双保险),Reason 以等宽块展示。
3308
+ * 重试按钮:进行中(retrying)时显示 Spinner 并禁用(防重复点击)。
3239
3309
  */
3240
- function ErrorBanner({ error, onRetry, retrying }) {
3310
+ function ErrorBanner({ error, onRetry, retrying, t = zhUiT }) {
3241
3311
  const [actionable] = (0, react.useState)(() => toActionableError(error));
3242
3312
  const reason = redact(actionable.reason);
3243
3313
  const item = actionable.item !== void 0 ? redact(actionable.item) : void 0;
@@ -3274,7 +3344,8 @@ function ErrorBanner({ error, onRetry, retrying }) {
3274
3344
  variant: "primary",
3275
3345
  onClick: onRetry,
3276
3346
  disabled: retrying === true,
3277
- children: retrying === true ? "…" : "Retry"
3347
+ loading: retrying === true,
3348
+ children: retrying === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Spinner, {}) : t("commonRetry")
3278
3349
  })
3279
3350
  })
3280
3351
  ]
@@ -3475,7 +3546,7 @@ function ExportSummary({ report }) {
3475
3546
  * 文本详情 = report.ts 渲染器的输出(已脱敏),展示前再过 redact() 双保险;
3476
3547
  * 以 <pre> 等宽块呈现保持对齐。
3477
3548
  */
3478
- function ReportView({ kind, exportReport, importResult, onAction, onDownload }) {
3549
+ function ReportView({ kind, exportReport, importResult, onAction, onDownload, downloadBusy = false, t = zhUiT }) {
3479
3550
  const actions = kind === "import" && importResult !== void 0 ? suggestedActions(importResult) : [];
3480
3551
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3481
3552
  className: config_manager_module_css_default.reportView,
@@ -3490,7 +3561,8 @@ function ReportView({ kind, exportReport, importResult, onAction, onDownload })
3490
3561
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
3491
3562
  variant: "primary",
3492
3563
  onClick: onDownload,
3493
- children: "Download"
3564
+ loading: downloadBusy,
3565
+ children: downloadBusy ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Spinner, {}) : t("export.download")
3494
3566
  })
3495
3567
  })
3496
3568
  ] }), kind === "import" && importResult !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
@@ -3554,6 +3626,10 @@ function ExportView({ api, t }) {
3554
3626
  const result = exp.result;
3555
3627
  const error = exp.error;
3556
3628
  const downloaded = exp.downloaded;
3629
+ /** 下载进行中(瞬态 UI:下载通常数秒,切 tab 后由 api 层继续,切回显示完成态) */
3630
+ const [downloading, setDownloading] = (0, react.useState)(false);
3631
+ /** 下载防重入 ref(导出完成自动下载 + 用户手动下载共享;避免双击并发下载同一文件) */
3632
+ const downloadingRef = (0, react.useRef)(false);
3557
3633
  const setMode = (next) => {
3558
3634
  runStore.patch({ export: { mode: next } });
3559
3635
  };
@@ -3614,15 +3690,21 @@ function ExportView({ api, t }) {
3614
3690
  runStore.patch({ export: { running: false } });
3615
3691
  }
3616
3692
  };
3617
- /** 把导出的 ZIP 下载到浏览器「下载」目录(默认静默下载,不弹另存为对话框) */
3693
+ /** 把导出的 ZIP 下载到浏览器「下载」目录(默认静默下载,不弹另存为对话框)。
3694
+ * 防重入:downloadingRef 作锁,进行中忽略重复点击/自动触发;失败后恢复可重试。 */
3618
3695
  const download = async (zipPath) => {
3619
- if (zipPath === "") return;
3696
+ if (zipPath === "" || downloadingRef.current) return;
3697
+ downloadingRef.current = true;
3698
+ setDownloading(true);
3620
3699
  try {
3621
3700
  runStore.patch({ export: { error: null } });
3622
3701
  await api.download(zipPath);
3623
3702
  runStore.patch({ export: { downloaded: true } });
3624
3703
  } catch (err) {
3625
3704
  runStore.patch({ export: { error: err instanceof Error ? err.message : String(err) } });
3705
+ } finally {
3706
+ downloadingRef.current = false;
3707
+ setDownloading(false);
3626
3708
  }
3627
3709
  };
3628
3710
  const customWarnings = mode === "custom" ? flow.validateSelection(selection).warnings : [];
@@ -3814,14 +3896,17 @@ function ExportView({ api, t }) {
3814
3896
  onRetry: () => {
3815
3897
  runExport();
3816
3898
  },
3817
- retrying: running
3899
+ retrying: running,
3900
+ t: api.t
3818
3901
  }),
3819
3902
  result !== null && !running && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReportView, {
3820
3903
  kind: "export",
3821
3904
  exportReport: result.report,
3822
3905
  onDownload: () => {
3823
3906
  download(result.zipPath);
3824
- }
3907
+ },
3908
+ downloadBusy: downloading,
3909
+ t: api.t
3825
3910
  }), downloaded && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Banner, {
3826
3911
  kind: "ok",
3827
3912
  children: t("export.saved", { name: result.report.file.name })
@@ -4166,22 +4251,62 @@ function SecretsForm({ missing, t, onChange }) {
4166
4251
  * (逐计划项操作 `▶/✓/⚠/✗/–` + 子进程命令行 `$ dsh plugin …`)。
4167
4252
  * - 数据来自 Host RunRegistry(经 /progress 轮询回传),行文本仅非敏感内容,
4168
4253
  * 渲染前再过 redact() 兜底(安全不变量:UI 展示文本先脱敏);
4169
- * - 限高内滚(logScroll)+ 新行自动滚到底部(新命令持续追加时无需手动滚动)。
4254
+ * - 限高内滚(logScroll);**智能自动滚动**:仅当用户贴近底部时跟随最新行;
4255
+ * 用户向上滚动查看历史时不强制拉回,改显示「↓ 新输出」提示,点击再滚到底部;
4256
+ * - memo 自定义比较:lines 数组为同一引用被 append(RunState.log push 不换引用),
4257
+ * 按引用浅比较无法感知新行 —— 比较长度 + t 引用,避免整页轮询反复重渲染整个列表。
4170
4258
  */
4171
- function ImportLogPanel({ lines, t }) {
4259
+ function ImportLogPanelBase({ lines, t }) {
4172
4260
  const scrollRef = (0, react.useRef)(null);
4261
+ /** 是否贴底(用户上滚置 false;滚动回底部自动恢复) */
4262
+ const stickRef = (0, react.useRef)(true);
4263
+ /** 用户上滚后是否有新行到达(显示「↓ 新输出」;点击跳到底部清除) */
4264
+ const [hasNewOutput, setHasNewOutput] = (0, react.useState)(false);
4265
+ /**
4266
+ * 上次渲染的数组引用(新输出 = 引用变化)。依赖 appendLog 的**不可变写入**:
4267
+ * 每次追加都生成新数组(run-registry.ts)——行数封顶后长度恒定,但引用必变,
4268
+ * 以引用判断才能感知截断后的新行(长度比较在 500 行封顶时失效)。
4269
+ */
4270
+ const prevLinesRef = (0, react.useRef)(lines);
4173
4271
  (0, react.useEffect)(() => {
4272
+ const el = scrollRef.current;
4273
+ if (el === null) return;
4274
+ const hasNew = lines !== prevLinesRef.current;
4275
+ prevLinesRef.current = lines;
4276
+ if (stickRef.current) {
4277
+ el.scrollTop = el.scrollHeight;
4278
+ setHasNewOutput(false);
4279
+ } else if (hasNew) setHasNewOutput(true);
4280
+ }, [lines]);
4281
+ /** 滚动中更新贴底状态(上滚 → 停止跟随;滚回底部 → 恢复跟随并清除提示) */
4282
+ const onScroll = () => {
4283
+ const el = scrollRef.current;
4284
+ if (el === null) return;
4285
+ const nearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 48;
4286
+ stickRef.current = nearBottom;
4287
+ if (nearBottom) setHasNewOutput(false);
4288
+ };
4289
+ /** 「↓ 新输出」:跳到底部 + 恢复跟随 */
4290
+ const jumpToBottom = () => {
4174
4291
  const el = scrollRef.current;
4175
4292
  if (el !== null) el.scrollTop = el.scrollHeight;
4176
- }, [lines.length]);
4293
+ stickRef.current = true;
4294
+ setHasNewOutput(false);
4295
+ };
4177
4296
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4178
4297
  className: config_manager_module_css_default.logPanel,
4179
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4298
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4180
4299
  className: config_manager_module_css_default.logHeader,
4181
- children: t("import.log.title")
4300
+ children: [t("import.log.title"), hasNewOutput && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4301
+ type: "button",
4302
+ className: config_manager_module_css_default.logJumpButton,
4303
+ onClick: jumpToBottom,
4304
+ children: ["↓ ", t("import.log.newOutput")]
4305
+ })]
4182
4306
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4183
4307
  className: config_manager_module_css_default.logScroll,
4184
4308
  ref: scrollRef,
4309
+ onScroll,
4185
4310
  children: lines.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4186
4311
  className: config_manager_module_css_default.logEmpty,
4187
4312
  children: t("import.log.empty")
@@ -4192,6 +4317,10 @@ function ImportLogPanel({ lines, t }) {
4192
4317
  })]
4193
4318
  });
4194
4319
  }
4320
+ /** memo:lines 数组经 appendLog **不可变追加**(每次 append 换新引用,run-registry.ts)——
4321
+ * 自定义比较以「数组引用 + t 引用」为准:引用未变 = 无新输出,跳过整个列表重渲染;
4322
+ * 引用已变 = 有新行(含 500 行封顶后长度不变的情况),必须重渲染。 */
4323
+ const ImportLogPanel = (0, react.memo)(ImportLogPanelBase, (prev, next) => prev.lines === next.lines && prev.t === next.t);
4195
4324
  /**
4196
4325
  * 导入向导主视图。
4197
4326
  */
@@ -4345,10 +4474,12 @@ function ImportWizardView({ api, t }) {
4345
4474
  * 解密密码交给向导——整个导入只输入这一次密码,没有第二个密码校验页面。 */
4346
4475
  const onUnlockArchive = async () => {
4347
4476
  if (imp.zipPath === null) return;
4477
+ const generation = pickGeneration.current;
4348
4478
  setUnlocking(true);
4349
4479
  setArchiveUnlockError(null);
4350
4480
  try {
4351
4481
  const { refs } = await wizard.unlockArchive(imp.zipPath, archivePassword);
4482
+ if (generation !== pickGeneration.current) return;
4352
4483
  wizard.setDecryptPassword(archivePassword);
4353
4484
  runStore.patch({ import: {
4354
4485
  archiveUnlocked: true,
@@ -4357,9 +4488,11 @@ function ImportWizardView({ api, t }) {
4357
4488
  } });
4358
4489
  runStore.syncWizard();
4359
4490
  await wizard.selectZip(imp.zipPath);
4491
+ if (generation !== pickGeneration.current) return;
4360
4492
  runStore.syncWizard();
4361
4493
  runStore.patch({ import: { phase: "preview" } });
4362
4494
  } catch (err) {
4495
+ if (generation !== pickGeneration.current) return;
4363
4496
  setArchiveUnlockError(err instanceof Error ? err.message : String(err));
4364
4497
  runStore.patch({ import: { error: err instanceof Error ? err.message : String(err) } });
4365
4498
  runStore.syncWizard();
@@ -4475,7 +4608,8 @@ function ImportWizardView({ api, t }) {
4475
4608
  }),
4476
4609
  error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBanner, {
4477
4610
  error,
4478
- onRetry: resetWizard
4611
+ onRetry: resetWizard,
4612
+ t: api.t
4479
4613
  })
4480
4614
  ]
4481
4615
  });
@@ -4489,7 +4623,8 @@ function ImportWizardView({ api, t }) {
4489
4623
  }),
4490
4624
  error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBanner, {
4491
4625
  error,
4492
- onRetry: resetWizard
4626
+ onRetry: resetWizard,
4627
+ t: api.t
4493
4628
  }),
4494
4629
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorList, { errors: imp.errors })
4495
4630
  ]
@@ -4544,7 +4679,8 @@ function ImportWizardView({ api, t }) {
4544
4679
  error,
4545
4680
  onRetry: () => {
4546
4681
  goPreview();
4547
- }
4682
+ },
4683
+ t: api.t
4548
4684
  }),
4549
4685
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4550
4686
  className: config_manager_module_css_default.actionRow,
@@ -4622,7 +4758,8 @@ function ImportWizardView({ api, t }) {
4622
4758
  error,
4623
4759
  onRetry: () => {
4624
4760
  goPreview();
4625
- }
4761
+ },
4762
+ t: api.t
4626
4763
  }),
4627
4764
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4628
4765
  className: config_manager_module_css_default.actionRow,
@@ -4661,7 +4798,10 @@ function ImportWizardView({ api, t }) {
4661
4798
  setArchiveUnlockError(null);
4662
4799
  }
4663
4800
  }),
4664
- archiveUnlockError !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBanner, { error: archiveUnlockError }),
4801
+ archiveUnlockError !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBanner, {
4802
+ error: archiveUnlockError,
4803
+ t: api.t
4804
+ }),
4665
4805
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4666
4806
  className: config_manager_module_css_default.actionRow,
4667
4807
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
@@ -4820,7 +4960,8 @@ function ImportWizardView({ api, t }) {
4820
4960
  error,
4821
4961
  onRetry: () => {
4822
4962
  execute();
4823
- }
4963
+ },
4964
+ t: api.t
4824
4965
  })]
4825
4966
  });
4826
4967
  if (step === "importing") {
@@ -4856,7 +4997,10 @@ function ImportWizardView({ api, t }) {
4856
4997
  className: config_manager_module_css_default.hint,
4857
4998
  children: t("import.importing")
4858
4999
  }),
4859
- error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBanner, { error }),
5000
+ error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBanner, {
5001
+ error,
5002
+ t: api.t
5003
+ }),
4860
5004
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorList, { errors: imp.errors })
4861
5005
  ]
4862
5006
  });
@@ -4891,16 +5035,186 @@ function ImportWizardView({ api, t }) {
4891
5035
  kind: "warn",
4892
5036
  children: t("report.needsRestart")
4893
5037
  }),
4894
- error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBanner, { error })
5038
+ error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBanner, {
5039
+ error,
5040
+ t: api.t
5041
+ })
4895
5042
  ]
4896
5043
  });
4897
5044
  }
4898
5045
  return null;
4899
5046
  }
4900
5047
  //#endregion
4901
- //#region src/client/snapshots/SnapshotsPanel.tsx
5048
+ //#region src/client/common/ConfirmDialog.tsx
4902
5049
  /**
4903
- * 快照恢复面板(M4):列出快照 → 选择 → dry-run 计划预览 → 确认执行 → 诚实报告。
5050
+ * ConfirmDialog —— 确认弹窗(危险/重要操作的二次确认,DESIGN.md §8.11 / §14)。
5051
+ *
5052
+ * 背景(2026-08-20):用户要求「删除按钮需要弹窗弹出二次确认」,替代原先的
5053
+ * 「行内两步确认」(已从 DESIGN.md §14 移除,见 t8 设计)。本项目此前无任何
5054
+ * modal / fixed / z-index 元素,本组件是首个浮动层 —— DESIGN.md §15 Anti-pattern #7
5055
+ * 已登记豁免(z-index 100;浮动层必须先设计进 DESIGN.md 的流程满足)。
5056
+ *
5057
+ * 交互约定:
5058
+ * - 受控组件:open=false 时不渲染;open=true 渲染遮罩 + 居中卡片;
5059
+ * - 关闭三途径:遮罩点击(仅 e.target === currentTarget)、Esc 键、取消按钮;
5060
+ * (可选 backdropClose:遮罩点击 / Esc 走此回调而非 onCancel —— 用于「不再提示」类
5061
+ * 弹窗,用户点遮罩只是暂时关闭、不算表态;缺省时三途径一致走 onCancel);
5062
+ * - busy=true(或 onConfirm 返回 Promise 的自管 busy)时禁用一切关闭途径与确认按钮,
5063
+ * 防重复提交;
5064
+ * - 初始焦点在取消按钮(危险确认不默认落破坏性按钮);关闭后还原焦点到触发按钮
5065
+ * (open 前的 document.activeElement);
5066
+ * - 不做完整 focus trap(两按钮场景风险可接受,DESIGN.md §8.11 已注明)。
5067
+ *
5068
+ * 安全:message 由调用方传入(渲染前已 redact 兜底,与全站一致);本组件不触碰任何凭据。
5069
+ * 样式:全部走 --dsw-* token(遮罩 color-mix 半透明、卡片 bg-layer-2 + border-l1),
5070
+ * 仅新增 dialogMask/dialogCard/dialogHeader/dialogBody 四个类(config-manager.module.css)。
5071
+ */
5072
+ /**
5073
+ * 确认弹窗:遮罩 + 居中卡片 + 标题/正文/按钮区。
5074
+ * 自管 busy:onConfirm 返回 Promise 时置 busy 直到 resolve(reject 仍关闭 busy,错误由调用方处理)。
5075
+ */
5076
+ function ConfirmDialog({ open, title, message, confirmLabel, cancelLabel, danger, busy: busyProp, onConfirm, onCancel, backdropClose, children }) {
5077
+ const [selfBusy, setSelfBusy] = (0, react.useState)(false);
5078
+ const busy = busyProp === true || selfBusy;
5079
+ /** open 前的活动元素(关闭后还原焦点) */
5080
+ const prevFocus = (0, react.useRef)(null);
5081
+ /** 取消按钮 ref(初始焦点) */
5082
+ const cancelRef = (0, react.useRef)(null);
5083
+ /** 遮罩/Esc 关闭回调(缺省 = 取消按钮同一回调) */
5084
+ const handleBackdropClose = backdropClose ?? onCancel;
5085
+ (0, react.useEffect)(() => {
5086
+ if (open) {
5087
+ prevFocus.current = document.activeElement;
5088
+ requestAnimationFrame(() => {
5089
+ cancelRef.current?.focus();
5090
+ });
5091
+ return;
5092
+ }
5093
+ if (prevFocus.current !== null && prevFocus.current instanceof HTMLElement) {
5094
+ prevFocus.current.focus();
5095
+ prevFocus.current = null;
5096
+ }
5097
+ }, [open]);
5098
+ (0, react.useEffect)(() => {
5099
+ if (!open || busy) return;
5100
+ const close = backdropClose ?? onCancel;
5101
+ const onKey = (e) => {
5102
+ if (e.key === "Escape") close();
5103
+ };
5104
+ window.addEventListener("keydown", onKey);
5105
+ return () => {
5106
+ window.removeEventListener("keydown", onKey);
5107
+ };
5108
+ }, [
5109
+ open,
5110
+ busy,
5111
+ backdropClose,
5112
+ onCancel
5113
+ ]);
5114
+ if (!open) return null;
5115
+ const handleConfirm = () => {
5116
+ if (busy) return;
5117
+ const result = onConfirm();
5118
+ if (result instanceof Promise) {
5119
+ setSelfBusy(true);
5120
+ result.finally(() => {
5121
+ setSelfBusy(false);
5122
+ });
5123
+ }
5124
+ };
5125
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5126
+ className: config_manager_module_css_default.dialogMask,
5127
+ onMouseDown: (e) => {
5128
+ if (e.target === e.currentTarget && !busy) handleBackdropClose();
5129
+ },
5130
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5131
+ className: config_manager_module_css_default.dialogCard,
5132
+ role: "dialog",
5133
+ "aria-modal": "true",
5134
+ "aria-label": title,
5135
+ children: [
5136
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5137
+ className: config_manager_module_css_default.dialogHeader,
5138
+ children: title
5139
+ }),
5140
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5141
+ className: config_manager_module_css_default.dialogBody,
5142
+ children: [message !== void 0 && message !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: message }), children]
5143
+ }),
5144
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5145
+ className: config_manager_module_css_default.actionRow,
5146
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
5147
+ variant: danger === true ? "danger" : "primary",
5148
+ disabled: busy,
5149
+ loading: busy,
5150
+ onClick: () => {
5151
+ handleConfirm();
5152
+ },
5153
+ children: busy ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Spinner, {}) : confirmLabel ?? ""
5154
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5155
+ ref: cancelRef,
5156
+ type: "button",
5157
+ className: config_manager_module_css_default.ghostButton,
5158
+ disabled: busy,
5159
+ "aria-busy": busy || void 0,
5160
+ onClick: onCancel,
5161
+ children: cancelLabel ?? ""
5162
+ })]
5163
+ })
5164
+ ]
5165
+ })
5166
+ });
5167
+ }
5168
+ //#endregion
5169
+ //#region src/ui/backup-schedule.ts
5170
+ /** 可选的间隔档位(UI 展示顺序 = 由短到长)。 */
5171
+ const BACKUP_INTERVAL_OPTIONS = [
5172
+ "6h",
5173
+ "12h",
5174
+ "24h",
5175
+ "7d"
5176
+ ];
5177
+ /** 校验设置输入(host 侧保存路由与组件提交共用;接受 unknown 防御畸形/空 body)。 */
5178
+ function validateBackupScheduleDraft(draft) {
5179
+ if (draft === null || typeof draft !== "object" || Array.isArray(draft)) return {
5180
+ ok: false,
5181
+ error: "body must be an object with enabled (boolean) and interval"
5182
+ };
5183
+ const d = draft;
5184
+ if (typeof d["enabled"] !== "boolean") return {
5185
+ ok: false,
5186
+ error: "enabled must be a boolean"
5187
+ };
5188
+ if (!BACKUP_INTERVAL_OPTIONS.includes(d["interval"])) return {
5189
+ ok: false,
5190
+ error: "interval must be one of 6h/12h/24h/7d"
5191
+ };
5192
+ return {
5193
+ ok: true,
5194
+ value: {
5195
+ enabled: d["enabled"],
5196
+ interval: d["interval"]
5197
+ }
5198
+ };
5199
+ }
5200
+ /** 上次运行状态 → Badge kind(success→ok / skipped→info / failed→error / 未知→info)。 */
5201
+ function backupRunBadgeKind(status) {
5202
+ switch (status) {
5203
+ case "success": return "ok";
5204
+ case "failed": return "error";
5205
+ case "skipped": return "info";
5206
+ default: return "info";
5207
+ }
5208
+ }
5209
+ /** 判断草稿相对已保存配置是否有未保存修改(决定「保存设置」按钮可用性)。 */
5210
+ function backupDraftDirty(draft, saved) {
5211
+ if (saved === null) return true;
5212
+ return draft.enabled !== saved.enabled || draft.interval !== saved.interval;
5213
+ }
5214
+ //#endregion
5215
+ //#region src/client/snapshots/SnapshotsPanel.tsx
5216
+ /**
5217
+ * 快照恢复面板(M4):列出快照 → 选择 → dry-run 计划预览 → 确认执行 → 诚实报告。
4904
5218
  *
4905
5219
  * 数据流:api.snapshots() 加载列表;选择快照后 api.restoreSnapshot(id, true) 拿
4906
5220
  * 恢复计划(零写入预览);确认后 api.restoreSnapshot(id, false) 执行并展示报告
@@ -4908,6 +5222,12 @@ function ImportWizardView({ api, t }) {
4908
5222
  * 状态组件内自持(useState),同时经 toSnapshotsStoreSlice() 镜像进模块级 runStore:
4909
5223
  * 模块级单例保证「切 tab 不丢」,sessionStorage 白名单保证「刷新恢复」
4910
5224
  * (选中快照 / dry-run 计划 / 执行报告;快照列表本身可随时重载,不持久化)。
5225
+ *
5226
+ * 视图底部附「定时全量备份」设置卡(BackupScheduleCard,m-backup-schedule):
5227
+ * 开关 + 间隔档位(6h/12h/24h/7d)保存经 PUT /backup-schedule(host 校验 + 重排
5228
+ * 调度器);「立即备份」经 POST /backup-schedule/run 复用 BackupScheduler.runOnce
5229
+ * (同一时刻防重)。草稿镜像 runStore.snapshots.backupDraft(未保存修改切 tab /
5230
+ * 刷新保留)。安全:定时备份恒不含 secret、不加密;本卡无敏感字段。
4911
5231
  */
4912
5232
  const initial$3 = {
4913
5233
  status: "loading",
@@ -4952,12 +5272,15 @@ function actionKindLabel(kind) {
4952
5272
  /**
4953
5273
  * 从 runStore 恢复上次的快照面板状态(切 tab 回 / 刷新后挂载)。
4954
5274
  * 无敏感字段;plan/report 为纯数据,可安全序列化恢复。
5275
+ * running 来自 store 镜像(刷新后经 runStore.resume() 以宿主 /runs 为权威重新置位——
5276
+ * 持久化白名单恒为 false,绝不把浏览器陈旧状态当成恢复执行中的依据)。
4955
5277
  */
4956
5278
  function initFromStore$2() {
4957
5279
  const s = runStore.getSnapshot().snapshots;
4958
5280
  return {
4959
5281
  ...initial$3,
4960
5282
  selectedId: s.selectedId,
5283
+ running: s.running,
4961
5284
  plan: s.plan,
4962
5285
  report: s.report,
4963
5286
  actionError: s.actionError,
@@ -4970,15 +5293,27 @@ function SnapshotsPanel({ api, t }) {
4970
5293
  const stateRef = (0, react.useRef)(state);
4971
5294
  /** 挂载守卫:卸载后不再 setState(store 镜像仍执行,异步结果照常落库) */
4972
5295
  const mountedRef = (0, react.useRef)(true);
5296
+ /** dry-run 计划请求代数:快速切换快照时作废在途旧请求(防晚到响应覆盖新选择) */
5297
+ const planGeneration = (0, react.useRef)(0);
5298
+ /** 执行恢复的二次确认弹窗开关(危险操作,DESIGN.md §8.11 场景) */
5299
+ const [confirmOpen, setConfirmOpen] = (0, react.useState)(false);
4973
5300
  /**
4974
5301
  * 统一提交入口:更新 stateRef → 挂载时 setState → **总是**镜像进 runStore。
4975
5302
  * 关键:镜像不依赖 effect flush —— 异步操作(dry-run 计划/执行恢复)完成回调
4976
5303
  * 在组件已卸载(切走 tab)时也能把结果(plan/report)写进 store,切回恢复。
5304
+ * backupDraft 由 BackupScheduleCard 独立管理,此处从 store 读回原值避免覆盖。
4977
5305
  */
4978
5306
  const commit = (next) => {
4979
5307
  stateRef.current = next;
4980
5308
  if (mountedRef.current) setState(next);
4981
- runStore.patch({ snapshots: toSnapshotsStoreSlice(next) });
5309
+ const backupDraft = runStore.getSnapshot().snapshots.backupDraft;
5310
+ runStore.patch({ snapshots: {
5311
+ ...toSnapshotsStoreSlice({
5312
+ ...next,
5313
+ backupDraft
5314
+ }),
5315
+ backupDraft
5316
+ } });
4982
5317
  };
4983
5318
  const patch = (p) => commit({
4984
5319
  ...stateRef.current,
@@ -4987,7 +5322,14 @@ function SnapshotsPanel({ api, t }) {
4987
5322
  /** 卸载时置挂载守卫 + 最后镜像一次(防止「最后一次改动后立即切 tab」时丢状态)。 */
4988
5323
  (0, react.useEffect)(() => () => {
4989
5324
  mountedRef.current = false;
4990
- runStore.patch({ snapshots: toSnapshotsStoreSlice(stateRef.current) });
5325
+ const backupDraft = runStore.getSnapshot().snapshots.backupDraft;
5326
+ runStore.patch({ snapshots: {
5327
+ ...toSnapshotsStoreSlice({
5328
+ ...stateRef.current,
5329
+ backupDraft
5330
+ }),
5331
+ backupDraft
5332
+ } });
4991
5333
  }, []);
4992
5334
  const load = () => {
4993
5335
  patch({
@@ -5008,6 +5350,8 @@ function SnapshotsPanel({ api, t }) {
5008
5350
  };
5009
5351
  (0, react.useEffect)(load, [api]);
5010
5352
  const select = (id) => {
5353
+ const generation = planGeneration.current + 1;
5354
+ planGeneration.current = generation;
5011
5355
  patch({
5012
5356
  selectedId: id,
5013
5357
  plan: null,
@@ -5016,11 +5360,13 @@ function SnapshotsPanel({ api, t }) {
5016
5360
  planning: true
5017
5361
  });
5018
5362
  api.restoreSnapshot(id, true).then((res) => {
5363
+ if (generation !== planGeneration.current) return;
5019
5364
  patch({
5020
5365
  planning: false,
5021
5366
  plan: res.plan ?? null
5022
5367
  });
5023
5368
  }, (err) => {
5369
+ if (generation !== planGeneration.current) return;
5024
5370
  patch({
5025
5371
  planning: false,
5026
5372
  actionError: err instanceof Error ? err.message : String(err)
@@ -5034,6 +5380,8 @@ function SnapshotsPanel({ api, t }) {
5034
5380
  report: null,
5035
5381
  actionError: null
5036
5382
  });
5383
+ setConfirmOpen(false);
5384
+ runStore.watchRunning("restore", 500);
5037
5385
  api.restoreSnapshot(state.selectedId, false).then((res) => {
5038
5386
  patch({
5039
5387
  running: false,
@@ -5044,6 +5392,8 @@ function SnapshotsPanel({ api, t }) {
5044
5392
  running: false,
5045
5393
  actionError: err instanceof Error ? err.message : String(err)
5046
5394
  });
5395
+ }).finally(() => {
5396
+ runStore.stopRunWatch("restore");
5047
5397
  });
5048
5398
  };
5049
5399
  const summary = () => {
@@ -5111,6 +5461,7 @@ function SnapshotsPanel({ api, t }) {
5111
5461
  "aria-selected": selected,
5112
5462
  "data-active": selected ? "" : void 0,
5113
5463
  className: config_manager_module_css_default.snapshotRow,
5464
+ disabled: state.planning || state.running,
5114
5465
  onClick: () => {
5115
5466
  select(meta.id);
5116
5467
  },
@@ -5166,8 +5517,9 @@ function SnapshotsPanel({ api, t }) {
5166
5517
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
5167
5518
  variant: "danger",
5168
5519
  disabled: state.running || state.plan.actions.every((a) => a.kind === "skip"),
5169
- onClick: execute,
5170
- title: t("snapshots.confirmRestore"),
5520
+ onClick: () => {
5521
+ setConfirmOpen(true);
5522
+ },
5171
5523
  children: state.running ? t("snapshots.executing") : t("snapshots.execute")
5172
5524
  })
5173
5525
  }),
@@ -5184,7 +5536,238 @@ function SnapshotsPanel({ api, t }) {
5184
5536
  reportLine(t("snapshots.failed"), state.report.failed.map((f) => `${f.item}: ${f.reason}`), true),
5185
5537
  reportLine(t("snapshots.skipped"), state.report.skipped, false)
5186
5538
  ] })
5187
- ] })
5539
+ ] }),
5540
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BackupScheduleCard, {
5541
+ api,
5542
+ t
5543
+ }),
5544
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfirmDialog, {
5545
+ open: confirmOpen,
5546
+ title: t("snapshots.confirmTitle"),
5547
+ message: t("snapshots.confirmRestore"),
5548
+ confirmLabel: t("snapshots.execute"),
5549
+ cancelLabel: t("common.cancel"),
5550
+ danger: true,
5551
+ busy: state.running,
5552
+ onConfirm: execute,
5553
+ onCancel: () => {
5554
+ setConfirmOpen(false);
5555
+ }
5556
+ })
5557
+ ]
5558
+ });
5559
+ }
5560
+ /** 间隔档位 → 字典键(t 的类型是字面量联合,switch 保持类型安全)。 */
5561
+ function intervalLabel$1(t, interval) {
5562
+ switch (interval) {
5563
+ case "6h": return t("backupSchedule.interval.6h");
5564
+ case "12h": return t("backupSchedule.interval.12h");
5565
+ case "24h": return t("backupSchedule.interval.24h");
5566
+ case "7d": return t("backupSchedule.interval.7d");
5567
+ }
5568
+ }
5569
+ /** 上次运行状态 → 字典键。 */
5570
+ function runStatusLabel(t, status) {
5571
+ switch (status) {
5572
+ case "success": return t("backupSchedule.status.success");
5573
+ case "skipped": return t("backupSchedule.status.skipped");
5574
+ case "failed": return t("backupSchedule.status.failed");
5575
+ default: return "—";
5576
+ }
5577
+ }
5578
+ function formatRunTime(iso) {
5579
+ if (iso === void 0 || iso === "") return "";
5580
+ const d = new Date(iso);
5581
+ return Number.isNaN(d.getTime()) ? iso : d.toLocaleString();
5582
+ }
5583
+ /**
5584
+ * 定时全量备份设置卡:总开关 + 间隔档位 + 上次运行状态 + 保存 / 立即备份。
5585
+ * 状态自持;草稿镜像 runStore.snapshots.backupDraft(未保存修改切 tab/刷新保留),
5586
+ * 保存成功清草稿(宿主配置为权威)。
5587
+ */
5588
+ function BackupScheduleCard({ api, t }) {
5589
+ const [status, setStatus] = (0, react.useState)("loading");
5590
+ const [error, setError] = (0, react.useState)(null);
5591
+ const [draft, setDraft] = (0, react.useState)({
5592
+ enabled: false,
5593
+ interval: "24h"
5594
+ });
5595
+ const [saved, setSaved] = (0, react.useState)(null);
5596
+ const [saving, setSaving] = (0, react.useState)(false);
5597
+ const [running, setRunning] = (0, react.useState)(false);
5598
+ const [lastRun, setLastRun] = (0, react.useState)(void 0);
5599
+ const [lastRunDetail, setLastRunDetail] = (0, react.useState)(null);
5600
+ const [flash, setFlash] = (0, react.useState)(null);
5601
+ const [actionError, setActionError] = (0, react.useState)(null);
5602
+ /** 挂载守卫:切 tab 卸载后异步回调只更新 store(草稿),不再 setState(React 18 无告警但浪费) */
5603
+ const mountedRef = (0, react.useRef)(true);
5604
+ (0, react.useEffect)(() => () => {
5605
+ mountedRef.current = false;
5606
+ }, []);
5607
+ const load = () => {
5608
+ setStatus("loading");
5609
+ setError(null);
5610
+ api.backupSchedule().then((schedule) => {
5611
+ if (!mountedRef.current) return;
5612
+ setSaved(schedule);
5613
+ setDraft(runStore.getSnapshot().snapshots.backupDraft ?? {
5614
+ enabled: schedule.enabled,
5615
+ interval: schedule.interval
5616
+ });
5617
+ setLastRun(schedule.lastRunStatus);
5618
+ setLastRunDetail(formatRunTime(schedule.lastRunAt));
5619
+ setStatus("ready");
5620
+ }, (err) => {
5621
+ if (!mountedRef.current) return;
5622
+ setStatus("error");
5623
+ setError(err instanceof Error ? err.message : String(err));
5624
+ });
5625
+ };
5626
+ (0, react.useEffect)(load, [api]);
5627
+ const updateDraft = (next) => {
5628
+ setDraft(next);
5629
+ runStore.patch({ snapshots: { backupDraft: next } });
5630
+ };
5631
+ const save = () => {
5632
+ if (saving || running) return;
5633
+ const parsed = validateBackupScheduleDraft(draft);
5634
+ if (!parsed.ok) {
5635
+ setActionError(parsed.error);
5636
+ return;
5637
+ }
5638
+ setSaving(true);
5639
+ setFlash(null);
5640
+ setActionError(null);
5641
+ api.saveBackupSchedule(parsed.value).then((schedule) => {
5642
+ runStore.patch({ snapshots: { backupDraft: null } });
5643
+ if (!mountedRef.current) return;
5644
+ setSaved(schedule);
5645
+ setDraft({
5646
+ enabled: schedule.enabled,
5647
+ interval: schedule.interval
5648
+ });
5649
+ setLastRun(schedule.lastRunStatus);
5650
+ setLastRunDetail(formatRunTime(schedule.lastRunAt));
5651
+ setSaving(false);
5652
+ setFlash(t("backupSchedule.saved"));
5653
+ }, (err) => {
5654
+ if (!mountedRef.current) return;
5655
+ setSaving(false);
5656
+ setActionError(err instanceof Error ? err.message : String(err));
5657
+ });
5658
+ };
5659
+ const runNow = () => {
5660
+ if (running || saving) return;
5661
+ setRunning(true);
5662
+ setFlash(null);
5663
+ setActionError(null);
5664
+ api.runBackupNow().then((res) => {
5665
+ if (!mountedRef.current) return;
5666
+ setSaved(res.schedule);
5667
+ setLastRun(res.run.status);
5668
+ setLastRunDetail(res.run.zip !== void 0 && res.run.zip !== "" ? res.run.zip : res.run.skipReason !== void 0 ? res.run.skipReason : formatRunTime(res.schedule.lastRunAt));
5669
+ setRunning(false);
5670
+ }, (err) => {
5671
+ if (!mountedRef.current) return;
5672
+ setRunning(false);
5673
+ setActionError(err instanceof Error ? err.message : String(err));
5674
+ });
5675
+ };
5676
+ const dirty = backupDraftDirty(draft, saved);
5677
+ const busy = saving || running;
5678
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Card, {
5679
+ className: config_manager_module_css_default.card,
5680
+ children: [
5681
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5682
+ className: config_manager_module_css_default.groupLabel,
5683
+ children: t("backupSchedule.title")
5684
+ }),
5685
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5686
+ className: config_manager_module_css_default.hint,
5687
+ children: t("backupSchedule.hint")
5688
+ }),
5689
+ status === "loading" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Spinner, { label: t("backupSchedule.loading") }),
5690
+ status === "error" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Banner, {
5691
+ kind: "error",
5692
+ children: [t("backupSchedule.error"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
5693
+ variant: "primary",
5694
+ onClick: load,
5695
+ children: t("common.retry")
5696
+ })]
5697
+ }),
5698
+ status === "ready" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
5699
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
5700
+ label: t("backupSchedule.enabled"),
5701
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Checkbox, {
5702
+ checked: draft.enabled,
5703
+ onChange: (checked) => {
5704
+ updateDraft({
5705
+ ...draft,
5706
+ enabled: checked
5707
+ });
5708
+ },
5709
+ label: t("backupSchedule.enabledHint"),
5710
+ disabled: busy
5711
+ })
5712
+ }),
5713
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
5714
+ label: t("backupSchedule.interval"),
5715
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
5716
+ className: config_manager_module_css_default.select,
5717
+ value: draft.interval,
5718
+ disabled: busy,
5719
+ onChange: (event) => {
5720
+ updateDraft({
5721
+ ...draft,
5722
+ interval: event.target.value
5723
+ });
5724
+ },
5725
+ children: BACKUP_INTERVAL_OPTIONS.map((interval) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
5726
+ value: interval,
5727
+ children: intervalLabel$1(t, interval)
5728
+ }, interval))
5729
+ })
5730
+ }),
5731
+ lastRun !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5732
+ className: config_manager_module_css_default.statRow,
5733
+ children: [
5734
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5735
+ className: config_manager_module_css_default.hint,
5736
+ children: t("backupSchedule.lastRun")
5737
+ }),
5738
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
5739
+ kind: backupRunBadgeKind(lastRun),
5740
+ children: runStatusLabel(t, lastRun)
5741
+ }),
5742
+ lastRunDetail !== null && lastRunDetail !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5743
+ className: config_manager_module_css_default.hint,
5744
+ children: lastRunDetail
5745
+ })
5746
+ ]
5747
+ }),
5748
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5749
+ className: config_manager_module_css_default.actionRow,
5750
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
5751
+ variant: "primary",
5752
+ disabled: busy || !dirty,
5753
+ onClick: save,
5754
+ title: dirty ? void 0 : t("backupSchedule.saved"),
5755
+ children: saving ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Spinner, { label: t("backupSchedule.save") }) : t("backupSchedule.save")
5756
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
5757
+ disabled: busy || !(saved?.enabled ?? false),
5758
+ onClick: runNow,
5759
+ children: running ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Spinner, { label: t("backupSchedule.running") }) : t("backupSchedule.runNow")
5760
+ })]
5761
+ })
5762
+ ] }),
5763
+ flash !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Banner, {
5764
+ kind: "ok",
5765
+ children: flash
5766
+ }),
5767
+ actionError !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Banner, {
5768
+ kind: "error",
5769
+ children: actionError
5770
+ })
5188
5771
  ]
5189
5772
  });
5190
5773
  }
@@ -5420,13 +6003,17 @@ function SyncHistoryView(props) {
5420
6003
  const [loading, setLoading] = (0, react.useState)(true);
5421
6004
  const [error, setError] = (0, react.useState)(null);
5422
6005
  const [entries, setEntries] = (0, react.useState)([]);
6006
+ /** 重试计数(错误态点「重试」递增 → 重新加载) */
6007
+ const [reloadKey, setReloadKey] = (0, react.useState)(0);
5423
6008
  (0, react.useEffect)(() => {
5424
6009
  let cancelled = false;
6010
+ setLoading(true);
5425
6011
  (async () => {
5426
6012
  try {
5427
6013
  const data = await api.history();
5428
6014
  if (!cancelled) {
5429
6015
  setEntries(data.entries);
6016
+ setError(null);
5430
6017
  setLoading(false);
5431
6018
  }
5432
6019
  } catch (err) {
@@ -5439,7 +6026,7 @@ function SyncHistoryView(props) {
5439
6026
  return () => {
5440
6027
  cancelled = true;
5441
6028
  };
5442
- }, [api]);
6029
+ }, [api, reloadKey]);
5443
6030
  const rows = (0, react.useMemo)(() => projectSyncHistoryEntries(entries), [entries]);
5444
6031
  const snapshotRows = (0, react.useMemo)(() => rows.filter((r) => r.kind === "apply" || r.kind === "push" || r.kind === "pull" || r.kind === "rollback").map((r) => ({
5445
6032
  id: r.id,
@@ -5448,10 +6035,14 @@ function SyncHistoryView(props) {
5448
6035
  reviewCount: r.reviewCount ?? 0
5449
6036
  })), [rows]);
5450
6037
  if (loading) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Spinner, { label: t("common.loading") });
5451
- if (error) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Card, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5452
- className: "error",
5453
- children: error
5454
- }) });
6038
+ if (error) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBanner, {
6039
+ error,
6040
+ onRetry: () => {
6041
+ setReloadKey((k) => k + 1);
6042
+ },
6043
+ retrying: loading,
6044
+ t: api.t
6045
+ });
5455
6046
  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") })] });
5456
6047
  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", {
5457
6048
  className: "sync-history-table",
@@ -7506,125 +8097,6 @@ const MARKET_UPSTREAM_OWNER = "xiajiajun516";
7506
8097
  /** 官方收录目标仓库名(见 MARKET_UPSTREAM_OWNER) */
7507
8098
  const MARKET_UPSTREAM_REPO = "dsh-config-market";
7508
8099
  //#endregion
7509
- //#region src/client/common/ConfirmDialog.tsx
7510
- /**
7511
- * ConfirmDialog —— 确认弹窗(危险/重要操作的二次确认,DESIGN.md §8.11 / §14)。
7512
- *
7513
- * 背景(2026-08-20):用户要求「删除按钮需要弹窗弹出二次确认」,替代原先的
7514
- * 「行内两步确认」(已从 DESIGN.md §14 移除,见 t8 设计)。本项目此前无任何
7515
- * modal / fixed / z-index 元素,本组件是首个浮动层 —— DESIGN.md §15 Anti-pattern #7
7516
- * 已登记豁免(z-index 100;浮动层必须先设计进 DESIGN.md 的流程满足)。
7517
- *
7518
- * 交互约定:
7519
- * - 受控组件:open=false 时不渲染;open=true 渲染遮罩 + 居中卡片;
7520
- * - 关闭三途径:遮罩点击(仅 e.target === currentTarget)、Esc 键、取消按钮;
7521
- * (可选 backdropClose:遮罩点击 / Esc 走此回调而非 onCancel —— 用于「不再提示」类
7522
- * 弹窗,用户点遮罩只是暂时关闭、不算表态;缺省时三途径一致走 onCancel);
7523
- * - busy=true(或 onConfirm 返回 Promise 的自管 busy)时禁用一切关闭途径与确认按钮,
7524
- * 防重复提交;
7525
- * - 初始焦点在取消按钮(危险确认不默认落破坏性按钮);关闭后还原焦点到触发按钮
7526
- * (open 前的 document.activeElement);
7527
- * - 不做完整 focus trap(两按钮场景风险可接受,DESIGN.md §8.11 已注明)。
7528
- *
7529
- * 安全:message 由调用方传入(渲染前已 redact 兜底,与全站一致);本组件不触碰任何凭据。
7530
- * 样式:全部走 --dsw-* token(遮罩 color-mix 半透明、卡片 bg-layer-2 + border-l1),
7531
- * 仅新增 dialogMask/dialogCard/dialogHeader/dialogBody 四个类(config-manager.module.css)。
7532
- */
7533
- /**
7534
- * 确认弹窗:遮罩 + 居中卡片 + 标题/正文/按钮区。
7535
- * 自管 busy:onConfirm 返回 Promise 时置 busy 直到 resolve(reject 仍关闭 busy,错误由调用方处理)。
7536
- */
7537
- function ConfirmDialog({ open, title, message, confirmLabel, cancelLabel, danger, busy: busyProp, onConfirm, onCancel, backdropClose, children }) {
7538
- const [selfBusy, setSelfBusy] = (0, react.useState)(false);
7539
- const busy = busyProp === true || selfBusy;
7540
- /** open 前的活动元素(关闭后还原焦点) */
7541
- const prevFocus = (0, react.useRef)(null);
7542
- /** 取消按钮 ref(初始焦点) */
7543
- const cancelRef = (0, react.useRef)(null);
7544
- /** 遮罩/Esc 关闭回调(缺省 = 取消按钮同一回调) */
7545
- const handleBackdropClose = backdropClose ?? onCancel;
7546
- (0, react.useEffect)(() => {
7547
- if (open) {
7548
- prevFocus.current = document.activeElement;
7549
- requestAnimationFrame(() => {
7550
- cancelRef.current?.focus();
7551
- });
7552
- return;
7553
- }
7554
- if (prevFocus.current !== null && prevFocus.current instanceof HTMLElement) {
7555
- prevFocus.current.focus();
7556
- prevFocus.current = null;
7557
- }
7558
- }, [open]);
7559
- (0, react.useEffect)(() => {
7560
- if (!open || busy) return;
7561
- const close = backdropClose ?? onCancel;
7562
- const onKey = (e) => {
7563
- if (e.key === "Escape") close();
7564
- };
7565
- window.addEventListener("keydown", onKey);
7566
- return () => {
7567
- window.removeEventListener("keydown", onKey);
7568
- };
7569
- }, [
7570
- open,
7571
- busy,
7572
- backdropClose,
7573
- onCancel
7574
- ]);
7575
- if (!open) return null;
7576
- const handleConfirm = () => {
7577
- if (busy) return;
7578
- const result = onConfirm();
7579
- if (result instanceof Promise) {
7580
- setSelfBusy(true);
7581
- result.finally(() => {
7582
- setSelfBusy(false);
7583
- });
7584
- }
7585
- };
7586
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7587
- className: config_manager_module_css_default.dialogMask,
7588
- onMouseDown: (e) => {
7589
- if (e.target === e.currentTarget && !busy) handleBackdropClose();
7590
- },
7591
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7592
- className: config_manager_module_css_default.dialogCard,
7593
- role: "dialog",
7594
- "aria-modal": "true",
7595
- "aria-label": title,
7596
- children: [
7597
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7598
- className: config_manager_module_css_default.dialogHeader,
7599
- children: title
7600
- }),
7601
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7602
- className: config_manager_module_css_default.dialogBody,
7603
- children: [message !== void 0 && message !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: message }), children]
7604
- }),
7605
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7606
- className: config_manager_module_css_default.actionRow,
7607
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
7608
- variant: danger === true ? "danger" : "primary",
7609
- disabled: busy,
7610
- onClick: () => {
7611
- handleConfirm();
7612
- },
7613
- children: busy ? "…" : confirmLabel ?? ""
7614
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7615
- ref: cancelRef,
7616
- type: "button",
7617
- className: config_manager_module_css_default.ghostButton,
7618
- disabled: busy,
7619
- onClick: onCancel,
7620
- children: cancelLabel ?? ""
7621
- })]
7622
- })
7623
- ]
7624
- })
7625
- });
7626
- }
7627
- //#endregion
7628
8100
  //#region src/client/market/disclaimer.ts
7629
8101
  /** localStorage key 前缀(按 key 隔离三个操作) */
7630
8102
  const DISCLAIMER_STORAGE_PREFIX = "dsh-cm-market.disclaimer.";
@@ -8651,7 +9123,9 @@ function MyConfigsView({ meApi, api, importApi, syncApi, t, myItems, myItemsErro
8651
9123
  setDeletingId(null);
8652
9124
  }
8653
9125
  };
8654
- /** 装回本地(复用市场下载 + 逐分区批准链路;条目内容在用户自己的公开仓库,必须带 repo 来源) */
9126
+ /** 装回本地(复用市场下载 + 逐分区批准链路;条目内容在用户自己的公开仓库,必须带 repo 来源)
9127
+ * 竞态守卫:下载期间用户可切换装回另一条目(install.itemId 已变),晚到响应一律丢弃,
9128
+ * 防止「会话标题是 B、详情是 A」的串扰。 */
8655
9129
  const runDownload = async (entry) => {
8656
9130
  commitInstall({
8657
9131
  itemId: entry.id,
@@ -8663,13 +9137,15 @@ function MyConfigsView({ meApi, api, importApi, syncApi, t, myItems, myItemsErro
8663
9137
  });
8664
9138
  try {
8665
9139
  const detail = await api.download(entry.id, entry.repoUrl);
8666
- if (installRef.current !== null) commitInstall({
9140
+ if (installRef.current === null || installRef.current.itemId !== entry.id) return;
9141
+ commitInstall({
8667
9142
  ...installRef.current,
8668
9143
  detail,
8669
9144
  approvals: defaultApprovals(detail.plan)
8670
9145
  });
8671
9146
  } catch (err) {
8672
- if (installRef.current !== null) patchInstall({ error: err instanceof Error ? err.message : String(err) });
9147
+ if (installRef.current === null || installRef.current.itemId !== entry.id) return;
9148
+ patchInstall({ error: err instanceof Error ? err.message : String(err) });
8673
9149
  }
8674
9150
  };
8675
9151
  const runImport = async () => {
@@ -9235,7 +9711,7 @@ function MyConfigsView({ meApi, api, importApi, syncApi, t, myItems, myItemsErro
9235
9711
  children: t("myconfigs.item.update")
9236
9712
  }),
9237
9713
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
9238
- disabled: install !== null && install.itemId === view.id && install.detail === null,
9714
+ disabled: install !== null && install.detail === null,
9239
9715
  onClick: () => {
9240
9716
  openInstall(entry);
9241
9717
  },
@@ -9680,7 +10156,9 @@ function MarketPanel({ api, myConfigsApi, importApi, syncApi, t }) {
9680
10156
  });
9681
10157
  }
9682
10158
  };
9683
- /** 下载 + 校验单条目 → dry-run 详情预览(零写入)。自托管条目(带 repo)必须携带来源仓库。 */
10159
+ /** 下载 + 校验单条目 → dry-run 详情预览(零写入)。自托管条目(带 repo)必须携带来源仓库。
10160
+ * 竞态守卫:下载期间用户可发起另一条目下载(downloadingId 已变),晚到响应一律丢弃,
10161
+ * 防止「弹窗标题是 B、详情是 A」的串扰。 */
9684
10162
  const runDownload = async (item) => {
9685
10163
  patch({
9686
10164
  downloadingId: item.id,
@@ -9688,12 +10166,14 @@ function MarketPanel({ api, myConfigsApi, importApi, syncApi, t }) {
9688
10166
  });
9689
10167
  try {
9690
10168
  const detail = await api.download(item.id, item.repo);
10169
+ if (stateRef.current.downloadingId !== item.id) return;
9691
10170
  patch({
9692
10171
  downloadingId: null,
9693
10172
  detail,
9694
10173
  approvals: defaultApprovals(detail.plan)
9695
10174
  });
9696
10175
  } catch (err) {
10176
+ if (stateRef.current.downloadingId !== item.id) return;
9697
10177
  patch({
9698
10178
  downloadingId: null,
9699
10179
  error: err instanceof Error ? err.message : String(err)
@@ -9804,13 +10284,13 @@ function MarketPanel({ api, myConfigsApi, importApi, syncApi, t }) {
9804
10284
  className: config_manager_module_css_default.actionRow,
9805
10285
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
9806
10286
  variant: "primary",
9807
- disabled: state.refreshing || state.importing,
10287
+ disabled: state.refreshing || state.importing || state.browsing,
9808
10288
  onClick: () => {
9809
10289
  runRefresh();
9810
10290
  },
9811
10291
  children: state.refreshing ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Spinner, { label: t("config.refreshing") }) : t("config.refresh")
9812
10292
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
9813
- disabled: state.browsing,
10293
+ disabled: state.browsing || state.refreshing || state.importing,
9814
10294
  onClick: () => {
9815
10295
  runBrowse();
9816
10296
  },
@@ -10116,7 +10596,7 @@ function MarketPanel({ api, myConfigsApi, importApi, syncApi, t }) {
10116
10596
  })
10117
10597
  ]
10118
10598
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
10119
- disabled: state.downloadingId === it.id,
10599
+ disabled: state.downloadingId !== null,
10120
10600
  onClick: () => {
10121
10601
  openDownload(it);
10122
10602
  },
@@ -10659,6 +11139,7 @@ const zh$2 = {
10659
11139
  "import.importing": "正在导入…(插件安装较多时可能需要几分钟,请勿关闭页面)",
10660
11140
  "import.log.title": "导入日志",
10661
11141
  "import.log.empty": "等待命令输出…",
11142
+ "import.log.newOutput": "↓ 新输出",
10662
11143
  "import.skipCurrent": "跳过当前插件",
10663
11144
  "import.skipPending": "正在跳过…其余项将继续导入",
10664
11145
  "import.retrySkipped": "重试失败/跳过的项 ({count})",
@@ -10704,6 +11185,7 @@ const zh$2 = {
10704
11185
  "snapshots.noActions": "该快照无可用恢复动作(或全部跳过)。",
10705
11186
  "snapshots.execute": "执行恢复",
10706
11187
  "snapshots.executing": "恢复执行中…",
11188
+ "snapshots.confirmTitle": "确认恢复",
10707
11189
  "snapshots.confirmRestore": "确认执行恢复?当前文件会先备份到快照的 pre-restore 目录,插件通过官方 dsh plugin remove 卸载。",
10708
11190
  "snapshots.reportTitle": "恢复报告",
10709
11191
  "snapshots.restored": "已还原",
@@ -10711,6 +11193,26 @@ const zh$2 = {
10711
11193
  "snapshots.manualHints": "需人工处理",
10712
11194
  "snapshots.failed": "失败",
10713
11195
  "snapshots.skipped": "跳过",
11196
+ "backupSchedule.title": "定时全量备份",
11197
+ "backupSchedule.hint": "按固定间隔在后台自动导出全量备份 ZIP(恒不含 secret、不加密;要加密请手动导出)。配置存 sync/backup-schedule.json,随 self 分区备份 / 远程同步迁移。",
11198
+ "backupSchedule.enabled": "启用定时备份",
11199
+ "backupSchedule.enabledHint": "开启后 DSH 在后台按间隔自动备份;每次启动时立即执行一次(距上次运行不足 1 小时则跳过启动触发)。",
11200
+ "backupSchedule.interval": "备份间隔",
11201
+ "backupSchedule.interval.6h": "每 6 小时",
11202
+ "backupSchedule.interval.12h": "每 12 小时",
11203
+ "backupSchedule.interval.24h": "每 24 小时",
11204
+ "backupSchedule.interval.7d": "每 7 天",
11205
+ "backupSchedule.save": "保存设置",
11206
+ "backupSchedule.saved": "设置已保存",
11207
+ "backupSchedule.runNow": "立即备份",
11208
+ "backupSchedule.running": "备份中…",
11209
+ "backupSchedule.lastRun": "上次运行",
11210
+ "backupSchedule.never": "从未运行",
11211
+ "backupSchedule.status.success": "成功",
11212
+ "backupSchedule.status.skipped": "已跳过",
11213
+ "backupSchedule.status.failed": "失败",
11214
+ "backupSchedule.loading": "加载定时备份设置…",
11215
+ "backupSchedule.error": "定时备份设置加载失败",
10714
11216
  "report.export.title": "导出完成",
10715
11217
  "report.import.title": "导入结果",
10716
11218
  "report.rollback.title": "回滚报告",
@@ -10808,6 +11310,7 @@ const en$2 = {
10808
11310
  "import.importing": "Importing… (installing many plugins may take a few minutes; keep this page open)",
10809
11311
  "import.log.title": "Import Log",
10810
11312
  "import.log.empty": "Waiting for command output…",
11313
+ "import.log.newOutput": "↓ New output",
10811
11314
  "import.skipCurrent": "Skip current plugin",
10812
11315
  "import.skipPending": "Skipping… the rest of the import continues",
10813
11316
  "import.retrySkipped": "Retry failed/skipped items ({count})",
@@ -10853,6 +11356,7 @@ const en$2 = {
10853
11356
  "snapshots.noActions": "No restorable actions for this snapshot (or all skipped).",
10854
11357
  "snapshots.execute": "Restore now",
10855
11358
  "snapshots.executing": "Restoring…",
11359
+ "snapshots.confirmTitle": "Confirm restore",
10856
11360
  "snapshots.confirmRestore": "Confirm restore? Current files are backed up to the snapshot's pre-restore folder first; plugins are removed via the official dsh plugin remove.",
10857
11361
  "snapshots.reportTitle": "Restore Report",
10858
11362
  "snapshots.restored": "Restored",
@@ -10860,6 +11364,26 @@ const en$2 = {
10860
11364
  "snapshots.manualHints": "Manual action needed",
10861
11365
  "snapshots.failed": "Failed",
10862
11366
  "snapshots.skipped": "Skipped",
11367
+ "backupSchedule.title": "Scheduled Full Backups",
11368
+ "backupSchedule.hint": "Automatically export a full backup ZIP in the background on a fixed cadence (secrets are never included, never encrypted — use manual export for encryption). Config lives in sync/backup-schedule.json and migrates with the self section.",
11369
+ "backupSchedule.enabled": "Enable scheduled backups",
11370
+ "backupSchedule.enabledHint": "When on, DSH backs up automatically on the cadence below; one run fires on every startup (skipped if less than 1h since the last run).",
11371
+ "backupSchedule.interval": "Backup interval",
11372
+ "backupSchedule.interval.6h": "Every 6 hours",
11373
+ "backupSchedule.interval.12h": "Every 12 hours",
11374
+ "backupSchedule.interval.24h": "Every 24 hours",
11375
+ "backupSchedule.interval.7d": "Every 7 days",
11376
+ "backupSchedule.save": "Save settings",
11377
+ "backupSchedule.saved": "Settings saved",
11378
+ "backupSchedule.runNow": "Back up now",
11379
+ "backupSchedule.running": "Backing up…",
11380
+ "backupSchedule.lastRun": "Last run",
11381
+ "backupSchedule.never": "Never run",
11382
+ "backupSchedule.status.success": "Success",
11383
+ "backupSchedule.status.skipped": "Skipped",
11384
+ "backupSchedule.status.failed": "Failed",
11385
+ "backupSchedule.loading": "Loading scheduled backup settings…",
11386
+ "backupSchedule.error": "Failed to load scheduled backup settings",
10863
11387
  "report.export.title": "Export Complete",
10864
11388
  "report.import.title": "Import Result",
10865
11389
  "report.rollback.title": "Rollback Report",