@logictan/dsh-config-manager 0.1.60

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 (429) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +105 -0
  3. package/cordis.patch.yml +21 -0
  4. package/lib/adapters/agent-instructions.d.ts +13 -0
  5. package/lib/adapters/agent-instructions.js +46 -0
  6. package/lib/adapters/agent-presets.d.ts +13 -0
  7. package/lib/adapters/agent-presets.js +14 -0
  8. package/lib/adapters/credentials.d.ts +37 -0
  9. package/lib/adapters/credentials.js +122 -0
  10. package/lib/adapters/file-collection.d.ts +15 -0
  11. package/lib/adapters/file-collection.js +127 -0
  12. package/lib/adapters/index.d.ts +57 -0
  13. package/lib/adapters/index.js +55 -0
  14. package/lib/adapters/link-report.d.ts +16 -0
  15. package/lib/adapters/link-report.js +47 -0
  16. package/lib/adapters/mcp.d.ts +28 -0
  17. package/lib/adapters/mcp.js +178 -0
  18. package/lib/adapters/plugin-files.d.ts +18 -0
  19. package/lib/adapters/plugin-files.js +142 -0
  20. package/lib/adapters/plugins.d.ts +41 -0
  21. package/lib/adapters/plugins.js +603 -0
  22. package/lib/adapters/pnpm-workspace.d.ts +26 -0
  23. package/lib/adapters/pnpm-workspace.js +147 -0
  24. package/lib/adapters/prompts.d.ts +30 -0
  25. package/lib/adapters/prompts.js +231 -0
  26. package/lib/adapters/providers.d.ts +34 -0
  27. package/lib/adapters/providers.js +155 -0
  28. package/lib/adapters/self.d.ts +17 -0
  29. package/lib/adapters/self.js +71 -0
  30. package/lib/adapters/sessions.d.ts +14 -0
  31. package/lib/adapters/sessions.js +15 -0
  32. package/lib/adapters/settings.d.ts +28 -0
  33. package/lib/adapters/settings.js +165 -0
  34. package/lib/adapters/skills.d.ts +13 -0
  35. package/lib/adapters/skills.js +14 -0
  36. package/lib/adapters/test-helpers.d.ts +125 -0
  37. package/lib/adapters/test-helpers.js +236 -0
  38. package/lib/adapters/ui.d.ts +27 -0
  39. package/lib/adapters/ui.js +89 -0
  40. package/lib/adapters/workspaces.d.ts +13 -0
  41. package/lib/adapters/workspaces.js +106 -0
  42. package/lib/client.d.ts +1026 -0
  43. package/lib/client.js +10047 -0
  44. package/lib/core/analyzer.d.ts +99 -0
  45. package/lib/core/analyzer.js +934 -0
  46. package/lib/core/backup.d.ts +138 -0
  47. package/lib/core/backup.js +513 -0
  48. package/lib/core/boot-rescue.d.ts +146 -0
  49. package/lib/core/boot-rescue.js +618 -0
  50. package/lib/core/cache-cleaner.d.ts +31 -0
  51. package/lib/core/cache-cleaner.js +95 -0
  52. package/lib/core/config-lifecycle.d.ts +180 -0
  53. package/lib/core/config-lifecycle.js +481 -0
  54. package/lib/core/config-snapshot.d.ts +135 -0
  55. package/lib/core/config-snapshot.js +424 -0
  56. package/lib/core/config-state.d.ts +70 -0
  57. package/lib/core/config-state.js +177 -0
  58. package/lib/core/consult-source.d.ts +58 -0
  59. package/lib/core/consult-source.js +260 -0
  60. package/lib/core/crash-report.d.ts +84 -0
  61. package/lib/core/crash-report.js +317 -0
  62. package/lib/core/exporter.d.ts +68 -0
  63. package/lib/core/exporter.js +375 -0
  64. package/lib/core/ghost-sweep.d.ts +19 -0
  65. package/lib/core/ghost-sweep.js +52 -0
  66. package/lib/core/importer.d.ts +60 -0
  67. package/lib/core/importer.js +29 -0
  68. package/lib/core/index.d.ts +14 -0
  69. package/lib/core/index.js +14 -0
  70. package/lib/core/journal.d.ts +197 -0
  71. package/lib/core/journal.js +418 -0
  72. package/lib/core/local-plugin-host.d.ts +35 -0
  73. package/lib/core/local-plugin-host.js +120 -0
  74. package/lib/core/local-plugin-pack.d.ts +135 -0
  75. package/lib/core/local-plugin-pack.js +278 -0
  76. package/lib/core/messages.d.ts +310 -0
  77. package/lib/core/messages.js +621 -0
  78. package/lib/core/migration-consult.d.ts +117 -0
  79. package/lib/core/migration-consult.js +363 -0
  80. package/lib/core/migration-history.d.ts +153 -0
  81. package/lib/core/migration-history.js +411 -0
  82. package/lib/core/model-tools.d.ts +50 -0
  83. package/lib/core/model-tools.js +148 -0
  84. package/lib/core/msg-types.d.ts +8 -0
  85. package/lib/core/msg-types.js +6 -0
  86. package/lib/core/phase3-child-crash.d.ts +1 -0
  87. package/lib/core/phase3-child-crash.js +87 -0
  88. package/lib/core/phase3-host.d.ts +138 -0
  89. package/lib/core/phase3-host.js +359 -0
  90. package/lib/core/phase3-prod-child.d.ts +1 -0
  91. package/lib/core/phase3-prod-child.js +35 -0
  92. package/lib/core/phase4-crash-child.d.ts +1 -0
  93. package/lib/core/phase4-crash-child.js +103 -0
  94. package/lib/core/plugin-cli.d.ts +150 -0
  95. package/lib/core/plugin-cli.js +497 -0
  96. package/lib/core/reconcile.d.ts +120 -0
  97. package/lib/core/reconcile.js +389 -0
  98. package/lib/core/recovery-orchestrator.d.ts +94 -0
  99. package/lib/core/recovery-orchestrator.js +327 -0
  100. package/lib/core/restore.d.ts +162 -0
  101. package/lib/core/restore.js +703 -0
  102. package/lib/core/rollback.d.ts +12 -0
  103. package/lib/core/rollback.js +145 -0
  104. package/lib/core/run-registry.d.ts +85 -0
  105. package/lib/core/run-registry.js +157 -0
  106. package/lib/core/startup-barrier.d.ts +53 -0
  107. package/lib/core/startup-barrier.js +77 -0
  108. package/lib/core/types.d.ts +515 -0
  109. package/lib/core/types.js +27 -0
  110. package/lib/core/undo.d.ts +73 -0
  111. package/lib/core/undo.js +87 -0
  112. package/lib/core/validator.d.ts +18 -0
  113. package/lib/core/validator.js +77 -0
  114. package/lib/core/verify-recovery.d.ts +21 -0
  115. package/lib/core/verify-recovery.js +256 -0
  116. package/lib/core/watcher.d.ts +101 -0
  117. package/lib/core/watcher.js +219 -0
  118. package/lib/index.d.ts +238 -0
  119. package/lib/index.js +2764 -0
  120. package/lib/migrations/index.d.ts +29 -0
  121. package/lib/migrations/index.js +56 -0
  122. package/lib/migrations/v1-to-v2.d.ts +9 -0
  123. package/lib/migrations/v1-to-v2.js +10 -0
  124. package/lib/profiles/index.d.ts +4 -0
  125. package/lib/profiles/index.js +5 -0
  126. package/lib/profiles/profile-manager.d.ts +106 -0
  127. package/lib/profiles/profile-manager.js +543 -0
  128. package/lib/schema/config.d.ts +28 -0
  129. package/lib/schema/config.js +165 -0
  130. package/lib/schema/index.d.ts +24 -0
  131. package/lib/schema/index.js +26 -0
  132. package/lib/schema/manifest.d.ts +28 -0
  133. package/lib/schema/manifest.js +109 -0
  134. package/lib/schema/tombstones.d.ts +46 -0
  135. package/lib/schema/tombstones.js +64 -0
  136. package/lib/schema/types.d.ts +232 -0
  137. package/lib/schema/types.js +8 -0
  138. package/lib/schema/versions.d.ts +27 -0
  139. package/lib/schema/versions.js +50 -0
  140. package/lib/security/encryption.d.ts +85 -0
  141. package/lib/security/encryption.js +279 -0
  142. package/lib/security/index.d.ts +16 -0
  143. package/lib/security/index.js +17 -0
  144. package/lib/security/integrity.d.ts +30 -0
  145. package/lib/security/integrity.js +78 -0
  146. package/lib/security/redaction.d.ts +26 -0
  147. package/lib/security/redaction.js +95 -0
  148. package/lib/security/secret-scanner.d.ts +86 -0
  149. package/lib/security/secret-scanner.js +404 -0
  150. package/lib/security/vault.d.ts +59 -0
  151. package/lib/security/vault.js +151 -0
  152. package/lib/security/zip-security.d.ts +27 -0
  153. package/lib/security/zip-security.js +154 -0
  154. package/lib/sync/autosync-config.d.ts +39 -0
  155. package/lib/sync/autosync-config.js +151 -0
  156. package/lib/sync/autosync-scheduler.d.ts +139 -0
  157. package/lib/sync/autosync-scheduler.js +564 -0
  158. package/lib/sync/fs.d.ts +18 -0
  159. package/lib/sync/fs.js +49 -0
  160. package/lib/sync/git/git-transport.d.ts +100 -0
  161. package/lib/sync/git/git-transport.js +437 -0
  162. package/lib/sync/github-auth.d.ts +131 -0
  163. package/lib/sync/github-auth.js +264 -0
  164. package/lib/sync/layout.d.ts +43 -0
  165. package/lib/sync/layout.js +203 -0
  166. package/lib/sync/snapshot-json.d.ts +9 -0
  167. package/lib/sync/snapshot-json.js +107 -0
  168. package/lib/sync/sync-config.d.ts +81 -0
  169. package/lib/sync/sync-config.js +281 -0
  170. package/lib/sync/sync-engine.d.ts +269 -0
  171. package/lib/sync/sync-engine.js +591 -0
  172. package/lib/sync/sync-history.d.ts +42 -0
  173. package/lib/sync/sync-history.js +78 -0
  174. package/lib/sync/sync-selection.d.ts +29 -0
  175. package/lib/sync/sync-selection.js +127 -0
  176. package/lib/sync/sync-session.d.ts +40 -0
  177. package/lib/sync/sync-session.js +54 -0
  178. package/lib/sync/sync-state.d.ts +37 -0
  179. package/lib/sync/sync-state.js +108 -0
  180. package/lib/sync/transport.d.ts +75 -0
  181. package/lib/sync/transport.js +55 -0
  182. package/lib/sync/ui-prefs.d.ts +34 -0
  183. package/lib/sync/ui-prefs.js +109 -0
  184. package/lib/sync/webdav/webdav-transport.d.ts +77 -0
  185. package/lib/sync/webdav/webdav-transport.js +451 -0
  186. package/lib/ui/errors.d.ts +19 -0
  187. package/lib/ui/errors.js +81 -0
  188. package/lib/ui/export-flow.d.ts +82 -0
  189. package/lib/ui/export-flow.js +98 -0
  190. package/lib/ui/i18n.d.ts +289 -0
  191. package/lib/ui/i18n.js +581 -0
  192. package/lib/ui/migration-consult-view.d.ts +91 -0
  193. package/lib/ui/migration-consult-view.js +115 -0
  194. package/lib/ui/progress.d.ts +38 -0
  195. package/lib/ui/progress.js +87 -0
  196. package/lib/ui/report.d.ts +23 -0
  197. package/lib/ui/report.js +153 -0
  198. package/lib/ui/test-helpers.d.ts +69 -0
  199. package/lib/ui/test-helpers.js +154 -0
  200. package/lib/ui/types.d.ts +257 -0
  201. package/lib/ui/types.js +21 -0
  202. package/lib/utils/atomic-write.d.ts +88 -0
  203. package/lib/utils/atomic-write.js +399 -0
  204. package/lib/utils/env-lock.d.ts +393 -0
  205. package/lib/utils/env-lock.js +930 -0
  206. package/lib/utils/hashing.d.ts +17 -0
  207. package/lib/utils/hashing.js +41 -0
  208. package/lib/utils/json.d.ts +24 -0
  209. package/lib/utils/json.js +76 -0
  210. package/lib/utils/logger.d.ts +39 -0
  211. package/lib/utils/logger.js +82 -0
  212. package/lib/utils/paths.d.ts +42 -0
  213. package/lib/utils/paths.js +189 -0
  214. package/lib/utils/proxy.d.ts +69 -0
  215. package/lib/utils/proxy.js +455 -0
  216. package/lib/utils/recursive-walk.d.ts +24 -0
  217. package/lib/utils/recursive-walk.js +134 -0
  218. package/lib/utils/zip.d.ts +54 -0
  219. package/lib/utils/zip.js +285 -0
  220. package/package.json +188 -0
  221. package/src/adapters/agent-instructions.ts +48 -0
  222. package/src/adapters/agent-presets.ts +14 -0
  223. package/src/adapters/credentials.test.ts +90 -0
  224. package/src/adapters/credentials.ts +143 -0
  225. package/src/adapters/file-collection.ts +137 -0
  226. package/src/adapters/files.test.ts +353 -0
  227. package/src/adapters/index.ts +94 -0
  228. package/src/adapters/link-report.test.ts +65 -0
  229. package/src/adapters/link-report.ts +61 -0
  230. package/src/adapters/mcp.test.ts +96 -0
  231. package/src/adapters/mcp.ts +185 -0
  232. package/src/adapters/plugin-files.ts +146 -0
  233. package/src/adapters/plugins.test.ts +326 -0
  234. package/src/adapters/plugins.ts +615 -0
  235. package/src/adapters/pnpm-workspace.test.ts +87 -0
  236. package/src/adapters/pnpm-workspace.ts +150 -0
  237. package/src/adapters/prompts.test.ts +110 -0
  238. package/src/adapters/prompts.ts +230 -0
  239. package/src/adapters/providers.test.ts +126 -0
  240. package/src/adapters/providers.ts +173 -0
  241. package/src/adapters/roundtrip.test.ts +190 -0
  242. package/src/adapters/self.test.ts +92 -0
  243. package/src/adapters/self.ts +75 -0
  244. package/src/adapters/sessions.ts +15 -0
  245. package/src/adapters/settings.test.ts +100 -0
  246. package/src/adapters/settings.ts +187 -0
  247. package/src/adapters/skills.ts +14 -0
  248. package/src/adapters/test-helpers.ts +248 -0
  249. package/src/adapters/ui.test.ts +57 -0
  250. package/src/adapters/ui.ts +108 -0
  251. package/src/adapters/workspaces.test.ts +106 -0
  252. package/src/adapters/workspaces.ts +109 -0
  253. package/src/client/ConfigManagerSection.tsx +90 -0
  254. package/src/client/api.ts +66 -0
  255. package/src/client/client-types.ts +18 -0
  256. package/src/client/common/ErrorBanner.tsx +73 -0
  257. package/src/client/common/Icon.tsx +129 -0
  258. package/src/client/common/Modal.tsx +173 -0
  259. package/src/client/common/ToastViewport.tsx +73 -0
  260. package/src/client/common/toast-store.test.ts +192 -0
  261. package/src/client/common/toast-store.ts +181 -0
  262. package/src/client/common/toast-types.ts +7 -0
  263. package/src/client/common/ui.tsx +342 -0
  264. package/src/client/config-manager.module.css +2971 -0
  265. package/src/client/css-modules.d.ts +8 -0
  266. package/src/client/index.ts +65 -0
  267. package/src/client/locales.ts +56 -0
  268. package/src/client/lucide-icons.d.ts +38 -0
  269. package/src/client/run-store.test.ts +223 -0
  270. package/src/client/run-store.ts +372 -0
  271. package/src/client/sync/SyncConfirmView.tsx +379 -0
  272. package/src/client/sync/SyncHistoryView.test.ts +40 -0
  273. package/src/client/sync/SyncHistoryView.tsx +220 -0
  274. package/src/client/sync/SyncSettingsView.tsx +1535 -0
  275. package/src/client/sync/history-model.test.ts +194 -0
  276. package/src/client/sync/history-model.ts +195 -0
  277. package/src/client/sync/sync-api.test.ts +585 -0
  278. package/src/client/sync/sync-api.ts +543 -0
  279. package/src/client/sync/sync-locales.ts +416 -0
  280. package/src/client/sync/sync-push-preview.test.ts +52 -0
  281. package/src/client/sync/sync-view-v2.test.ts +213 -0
  282. package/src/client/sync/sync-view.test.ts +487 -0
  283. package/src/client/sync/sync-view.ts +759 -0
  284. package/src/core/analyzer.ts +1068 -0
  285. package/src/core/backup.ts +605 -0
  286. package/src/core/boot-rescue.test.ts +847 -0
  287. package/src/core/boot-rescue.ts +737 -0
  288. package/src/core/cache-cleaner.test.ts +169 -0
  289. package/src/core/cache-cleaner.ts +121 -0
  290. package/src/core/config-lifecycle.test.ts +727 -0
  291. package/src/core/config-lifecycle.ts +573 -0
  292. package/src/core/config-snapshot.test.ts +369 -0
  293. package/src/core/config-snapshot.ts +522 -0
  294. package/src/core/config-state.test.ts +381 -0
  295. package/src/core/config-state.ts +207 -0
  296. package/src/core/consult-source.test.ts +199 -0
  297. package/src/core/consult-source.ts +293 -0
  298. package/src/core/crash-report.test.ts +380 -0
  299. package/src/core/crash-report.ts +369 -0
  300. package/src/core/exporter.ts +417 -0
  301. package/src/core/ghost-sweep.test.ts +94 -0
  302. package/src/core/ghost-sweep.ts +67 -0
  303. package/src/core/importer.ts +83 -0
  304. package/src/core/index.ts +56 -0
  305. package/src/core/journal.test.ts +257 -0
  306. package/src/core/journal.ts +518 -0
  307. package/src/core/local-plugin-host.ts +152 -0
  308. package/src/core/local-plugin-pack.test.ts +359 -0
  309. package/src/core/local-plugin-pack.ts +363 -0
  310. package/src/core/messages.test.ts +54 -0
  311. package/src/core/messages.ts +664 -0
  312. package/src/core/migration-consult.test.ts +315 -0
  313. package/src/core/migration-consult.ts +494 -0
  314. package/src/core/migration-history.test.ts +319 -0
  315. package/src/core/migration-history.ts +484 -0
  316. package/src/core/model-tools.test.ts +190 -0
  317. package/src/core/model-tools.ts +208 -0
  318. package/src/core/msg-types.ts +10 -0
  319. package/src/core/phase3-child-crash.ts +95 -0
  320. package/src/core/phase3-host.ts +411 -0
  321. package/src/core/phase3-p1.test.ts +212 -0
  322. package/src/core/phase3-p2b-fingerprint.test.ts +189 -0
  323. package/src/core/phase3-prod-child.ts +36 -0
  324. package/src/core/phase3-production-integration.test.ts +286 -0
  325. package/src/core/phase4-crash-child.ts +101 -0
  326. package/src/core/phase4-crash-injection.test.ts +166 -0
  327. package/src/core/plugin-cli.fs.test.ts +184 -0
  328. package/src/core/plugin-cli.test.ts +137 -0
  329. package/src/core/plugin-cli.ts +563 -0
  330. package/src/core/reconcile.test.ts +392 -0
  331. package/src/core/reconcile.ts +498 -0
  332. package/src/core/recovery-orchestrator.test.ts +145 -0
  333. package/src/core/recovery-orchestrator.ts +366 -0
  334. package/src/core/restore-manage.test.ts +94 -0
  335. package/src/core/restore.ts +849 -0
  336. package/src/core/rollback.ts +151 -0
  337. package/src/core/run-progress.test.ts +237 -0
  338. package/src/core/run-registry.test.ts +232 -0
  339. package/src/core/run-registry.ts +209 -0
  340. package/src/core/smoke.test.ts +842 -0
  341. package/src/core/startup-barrier.ts +99 -0
  342. package/src/core/types.ts +542 -0
  343. package/src/core/undo.ts +116 -0
  344. package/src/core/validator.ts +84 -0
  345. package/src/core/verify-recovery.ts +265 -0
  346. package/src/core/watcher.test.ts +326 -0
  347. package/src/core/watcher.ts +263 -0
  348. package/src/index.facade.test.ts +265 -0
  349. package/src/index.sync.test.ts +217 -0
  350. package/src/index.ts +2975 -0
  351. package/src/migrations/index.ts +71 -0
  352. package/src/migrations/v1-to-v2.ts +17 -0
  353. package/src/profiles/index.ts +8 -0
  354. package/src/profiles/profile-manager.test.ts +282 -0
  355. package/src/profiles/profile-manager.ts +630 -0
  356. package/src/schema/config.ts +165 -0
  357. package/src/schema/index.ts +71 -0
  358. package/src/schema/manifest-schema.test.ts +519 -0
  359. package/src/schema/manifest.ts +124 -0
  360. package/src/schema/tombstones.test.ts +131 -0
  361. package/src/schema/tombstones.ts +89 -0
  362. package/src/schema/types.ts +245 -0
  363. package/src/schema/versions.ts +55 -0
  364. package/src/security/encryption.ts +335 -0
  365. package/src/security/index.ts +16 -0
  366. package/src/security/integrity.ts +102 -0
  367. package/src/security/redaction.ts +101 -0
  368. package/src/security/secret-scanner.ts +445 -0
  369. package/src/security/security.test.ts +1215 -0
  370. package/src/security/vault.test.ts +164 -0
  371. package/src/security/vault.ts +198 -0
  372. package/src/security/zip-security.ts +180 -0
  373. package/src/sync/autosync-config.test.ts +147 -0
  374. package/src/sync/autosync-config.ts +179 -0
  375. package/src/sync/autosync-scheduler.test.ts +490 -0
  376. package/src/sync/autosync-scheduler.ts +646 -0
  377. package/src/sync/fs.ts +48 -0
  378. package/src/sync/git/git-transport.test.ts +673 -0
  379. package/src/sync/git/git-transport.ts +491 -0
  380. package/src/sync/github-auth.test.ts +280 -0
  381. package/src/sync/github-auth.ts +356 -0
  382. package/src/sync/layout.test.ts +270 -0
  383. package/src/sync/layout.ts +222 -0
  384. package/src/sync/snapshot-json.test.ts +119 -0
  385. package/src/sync/snapshot-json.ts +128 -0
  386. package/src/sync/sync-config.test.ts +310 -0
  387. package/src/sync/sync-config.ts +299 -0
  388. package/src/sync/sync-engine.test.ts +1074 -0
  389. package/src/sync/sync-engine.ts +754 -0
  390. package/src/sync/sync-history.test.ts +116 -0
  391. package/src/sync/sync-history.ts +120 -0
  392. package/src/sync/sync-selection.test.ts +186 -0
  393. package/src/sync/sync-selection.ts +143 -0
  394. package/src/sync/sync-session.test.ts +143 -0
  395. package/src/sync/sync-session.ts +77 -0
  396. package/src/sync/sync-state.test.ts +164 -0
  397. package/src/sync/sync-state.ts +131 -0
  398. package/src/sync/transport.test.ts +111 -0
  399. package/src/sync/transport.ts +118 -0
  400. package/src/sync/ui-prefs.test.ts +152 -0
  401. package/src/sync/ui-prefs.ts +135 -0
  402. package/src/sync/webdav/webdav-transport.test.ts +871 -0
  403. package/src/sync/webdav/webdav-transport.ts +535 -0
  404. package/src/ui/errors.test.ts +47 -0
  405. package/src/ui/errors.ts +100 -0
  406. package/src/ui/export-flow.test.ts +87 -0
  407. package/src/ui/export-flow.ts +156 -0
  408. package/src/ui/i18n.ts +590 -0
  409. package/src/ui/migration-consult-view.test.ts +151 -0
  410. package/src/ui/migration-consult-view.ts +178 -0
  411. package/src/ui/progress.test.ts +47 -0
  412. package/src/ui/progress.ts +102 -0
  413. package/src/ui/report.test.ts +101 -0
  414. package/src/ui/report.ts +146 -0
  415. package/src/ui/test-helpers.ts +184 -0
  416. package/src/ui/types.ts +325 -0
  417. package/src/utils/atomic-write.test.ts +312 -0
  418. package/src/utils/atomic-write.ts +422 -0
  419. package/src/utils/env-lock.test.ts +999 -0
  420. package/src/utils/env-lock.ts +1086 -0
  421. package/src/utils/hashing.ts +53 -0
  422. package/src/utils/json.ts +77 -0
  423. package/src/utils/logger.ts +114 -0
  424. package/src/utils/paths.ts +176 -0
  425. package/src/utils/proxy.test.ts +465 -0
  426. package/src/utils/proxy.ts +493 -0
  427. package/src/utils/recursive-walk.test.ts +148 -0
  428. package/src/utils/recursive-walk.ts +145 -0
  429. package/src/utils/zip.ts +332 -0
@@ -0,0 +1,379 @@
1
+ /**
2
+ * 一键同步差异确认视图(方案 A §5 差异确认数据流)。
3
+ *
4
+ * 消费 POST /sync/sync 返回的 items[],渲染逐项确认列表:
5
+ * - 每项:kindTag + description + severity + 「采用远端」复选框(默认勾选,可取消);
6
+ * - Conflict 项:内联弹窗(用本地 / 用远端 / 跳过);
7
+ * - 底部:「确认导入」(apply-items)+「取消」(cancel)。
8
+ *
9
+ * 全部渲染模型来自 ./sync-view.ts 纯函数(node 单测覆盖),组件只做装配 + 交互状态。
10
+ */
11
+ import { useMemo, useState } from 'react'
12
+ import type { ReactNode } from 'react'
13
+ import { useEffect } from 'react'
14
+
15
+ import { Badge, Banner, Button, Spinner } from '../common/ui.tsx'
16
+ import { toast } from '../common/toast-store.ts'
17
+ import type { SyncApi, SyncConfirmItem } from './sync-api.ts'
18
+ import {
19
+ buildAdoptions, keepLocalAll, kindLabel, reviewItems, severityLabel,
20
+ summarizeConfirmItems, useRemoteAll, type SyncConflictResolution,
21
+ } from './sync-view.ts'
22
+ import type { ApplyItemsResponse } from './sync-api.ts'
23
+ import type { TranslateNS } from '../client-types.ts'
24
+ import type { SyncConfirmDecisions } from '../run-store.ts'
25
+ import css from '../config-manager.module.css'
26
+
27
+ export interface SyncConfirmViewProps {
28
+ api: SyncApi
29
+ /** 差异确认会话 id(apply-items / cancel 引用)。 */
30
+ syncSessionId: string
31
+ /** 被拉取的远端快照 id。 */
32
+ snapshotId: string
33
+ items: SyncConfirmItem[]
34
+ needsReview: boolean
35
+ /** 兼容性评级(可选展示)。 */
36
+ compatibility?: 'excellent' | 'good' | 'partial' | 'unsupported'
37
+ /** 翻译器。 */
38
+ t: TranslateNS<'config-manager-sync'>
39
+ /** 逐项决策(受控:来自 runStore.sync.confirmDecisions;null = 无持久化决策,按 defaultAdopt 初始化) */
40
+ decisions: SyncConfirmDecisions | null
41
+ /** 决策变更上抛(SyncSettingsView patch 镜像 runStore,切 tab/刷新不丢) */
42
+ onDecisionsChange: (decisions: SyncConfirmDecisions) => void
43
+ /** 用户取消确认后回调(复位到空闲态)。 */
44
+ onCancel: () => void
45
+ /** 回滚成功后回调(清空 lastRestoreId)。 */
46
+ onRollbackDone?: () => void
47
+ }
48
+
49
+ /** 冲突解决方式:与导入恢复向导一致的两项(保留当前 / 使用备份)。 */
50
+ type ConflictResolution = SyncConflictResolution
51
+
52
+ /** 单条差异项的可变决策状态。 */
53
+ interface ItemState {
54
+ adopted: boolean
55
+ resolution?: ConflictResolution
56
+ }
57
+
58
+ export function SyncConfirmView(props: SyncConfirmViewProps): ReactNode {
59
+ const { api, syncSessionId, snapshotId, items, needsReview, compatibility, t, decisions, onDecisionsChange, onCancel, onRollbackDone } = props;
60
+ // 逐项决策状态(受控:来自 props.decisions;null 时按 defaultAdopt 初始化;持久化决策覆盖同名项)
61
+ const states = useMemo<Record<string, ItemState>>(() => {
62
+ const base: Record<string, ItemState> = {};
63
+ for (const it of items) base[it.itemId] = { adopted: it.defaultAdopt, resolution: undefined };
64
+ if (decisions === null) return base;
65
+ for (const [id, d] of Object.entries(decisions)) {
66
+ if (base[id] !== undefined) base[id] = { adopted: d.adopted, resolution: d.resolution };
67
+ }
68
+ return base;
69
+ }, [items, decisions]);
70
+ // 执行阶段:'idle' | 'applying' | 'done' | 'failed' | 'cancelling'
71
+ const [phase, setPhase] = useState<'idle' | 'applying' | 'done' | 'failed' | 'cancelling'>('idle');
72
+ const [applyResult, setApplyResult] = useState<ApplyItemsResponse | null>(null);
73
+ const [error, setError] = useState<string | null>(null);
74
+ const summary = useMemo(() => summarizeConfirmItems(
75
+ items.map((it) => ({ ...it, adopt: states[it.itemId]?.adopted ?? it.defaultAdopt })),
76
+ ), [items, states]);
77
+
78
+ // 仅需人工决策的项进入确认列表;非决策项(Create/Update 等)默认自动采用,不逐项展示
79
+ const displayItems = useMemo(() => reviewItems(items), [items]);
80
+
81
+ const setAdopted = (itemId: string, adopted: boolean): void => {
82
+ const existing: ItemState = states[itemId] ?? { adopted: false, resolution: undefined };
83
+ onDecisionsChange({ ...(decisions ?? {}), [itemId]: { adopted, resolution: existing.resolution } });
84
+ };
85
+
86
+ const setResolution = (itemId: string, resolution: ConflictResolution): void => {
87
+ const existing: ItemState = states[itemId] ?? { adopted: false, resolution: undefined };
88
+ onDecisionsChange({ ...(decisions ?? {}), [itemId]: { adopted: existing.adopted, resolution } });
89
+ };
90
+
91
+ // 批量决策(仅作用于 Conflict 项;非 Conflict 项的 adopt 保持默认)
92
+ const applyBulkDecision = (bulk: readonly { itemId: string; resolution: ConflictResolution; adopt: boolean }[]): void => {
93
+ if (bulk.length === 0) return;
94
+ const next: SyncConfirmDecisions = { ...(decisions ?? {}) };
95
+ for (const d of bulk) {
96
+ const existing: ItemState = states[d.itemId] ?? { adopted: false, resolution: undefined };
97
+ next[d.itemId] = { ...existing, resolution: d.resolution, adopted: d.adopt };
98
+ }
99
+ onDecisionsChange(next);
100
+ };
101
+
102
+ const runApply = async (): Promise<void> => {
103
+ // 校验冲突项必须已解决(与 buildAdoptions 的强制先解决一致)
104
+ const unresolved = items.find(
105
+ (it) => it.kind === 'Conflict' && (states[it.itemId]?.adopted === true) && states[it.itemId]?.resolution === undefined,
106
+ );
107
+ if (unresolved !== undefined) {
108
+ setError(t('syncflow.conflictUnresolved', { itemId: unresolved.itemId }));
109
+ return;
110
+ }
111
+ setPhase('applying');
112
+ setError(null);
113
+ try {
114
+ const adoptedMap = new Map<string, boolean>();
115
+ for (const it of items) {
116
+ adoptedMap.set(it.itemId, states[it.itemId]?.adopted ?? false);
117
+ }
118
+ const resolutions = new Map<string, ConflictResolution>();
119
+ for (const it of items) {
120
+ const res = states[it.itemId]?.resolution;
121
+ if (res !== undefined) resolutions.set(it.itemId, res);
122
+ }
123
+ const adoptions = buildAdoptions(items, adoptedMap, resolutions);
124
+ const result = await api.applyItems({ syncSessionId, adoptions });
125
+ setPhase(result.ok ? 'done' : 'failed');
126
+ setApplyResult(result);
127
+ // R-05:导入成功改走 Toast —— 在此处(而非 ApplyResultCard 渲染期)触发,
128
+ // 保证每次 apply 恰好一次、且重渲染不会重复弹。失败分支**不**弹 Toast,
129
+ // 由 ApplyResultCard 的 error Banner 承载(K-06:同卡内还有 warnings 明细与
130
+ // 回滚危险按钮,用户必须停在此处决策)。
131
+ if (result.ok) {
132
+ toast.ok(t('syncflow.importDone', { n: String(result.applied.length) }));
133
+ }
134
+ } catch (err) {
135
+ setPhase('failed');
136
+ setError(err instanceof Error ? err.message : String(err));
137
+ }
138
+ };
139
+
140
+ const runCancel = async (): Promise<void> => {
141
+ setPhase('cancelling');
142
+ setError(null);
143
+ try {
144
+ await api.cancel(syncSessionId);
145
+ } catch { /* 取消失败无需打扰 */ }
146
+ onCancel();
147
+ };
148
+
149
+ const runRollback = async (restoreId: string): Promise<void> => {
150
+ setPhase('applying');
151
+ setError(null);
152
+ try {
153
+ await api.rollback({ restoreId });
154
+ setPhase('done');
155
+ setApplyResult(null);
156
+ // R-04:回滚成功改走 Toast(原 ok Banner 只在「回滚成功」这一条路径出现;
157
+ // 操作已完成、用户可离开,不需要停留阅读)。下方「关闭」按钮保留。
158
+ toast.ok(t('syncflow.rollbackDone'));
159
+ onRollbackDone?.();
160
+ } catch (err) {
161
+ setPhase('failed');
162
+ setError(err instanceof Error ? err.message : String(err));
163
+ }
164
+ };
165
+
166
+ const busy = phase === 'applying' || phase === 'cancelling';
167
+
168
+ if (items.length === 0) {
169
+ return (
170
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
171
+ <Banner kind="ok">{t('syncflow.empty')}</Banner>
172
+ <div className={css.actionRow}>
173
+ <Button disabled={busy} onClick={() => { void runCancel() }}>
174
+ {t('common.close')}
175
+ </Button>
176
+ </div>
177
+ </div>
178
+ );
179
+ }
180
+
181
+ return (
182
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
183
+ {compatibility !== undefined && (
184
+ <div className={css.statRow}>
185
+ <Badge kind="info">{compatibility}</Badge>
186
+ </div>
187
+ )}
188
+ <Banner kind={needsReview ? 'warn' : 'info'}>
189
+ {needsReview ? t('syncflow.needsReviewBadge') : t('syncflow.diffCount', { count: String(summary.total) })}
190
+ </Banner>
191
+ <div className={css.statRow}>
192
+ <Badge kind="info">{t('syncflow.diffCount', { count: String(summary.total) })}</Badge>
193
+ <Badge kind="ok">{t('syncflow.adoptRemote')} {summary.adopted}</Badge>
194
+ {summary.error > 0 && <Badge kind="error">{severityLabel('error', api.t)} × {summary.error}</Badge>}
195
+ {summary.warning > 0 && <Badge kind="warn">{severityLabel('warning', api.t)} × {summary.warning}</Badge>}
196
+ </div>
197
+ <span className={css.hint}>{t('syncflow.adoptHint')}</span>
198
+
199
+ {/* 批量决策(仅作用于 Conflict 项) */}
200
+ <div className={css.actionRow}>
201
+ <Button variant="ghost" disabled={busy || displayItems.filter((it) => it.kind === 'Conflict').length === 0} onClick={() => { applyBulkDecision(keepLocalAll(items)) }}>
202
+ {t('syncflow.keepLocalAll')}
203
+ </Button>
204
+ <Button variant="primary" disabled={busy || displayItems.filter((it) => it.kind === 'Conflict').length === 0} onClick={() => { applyBulkDecision(useRemoteAll(items)) }}>
205
+ {t('syncflow.useRemoteAll')}
206
+ </Button>
207
+ <span className={css.hint}>{t('syncflow.bulkHint')}</span>
208
+ </div>
209
+
210
+ {/* 逐项差异列表(仅需人工决策的项);固定容器限高 + 内部滚动,防止整页被拉长 */}
211
+ <div className={css.confirmScroll}>
212
+ <div className={css.reportList}>
213
+ {displayItems.map((it) => {
214
+ const st = states[it.itemId] ?? { adopted: it.defaultAdopt };
215
+ const isConflict = it.kind === 'Conflict';
216
+ return (
217
+ <div key={it.itemId} className={css.statRow}>
218
+ <label className={css.checkboxRow}>
219
+ <input
220
+ type="checkbox"
221
+ checked={st.adopted}
222
+ disabled={busy}
223
+ onChange={(e) => { setAdopted(it.itemId, e.target.checked) }}
224
+ />
225
+ <span>{kindLabel(it.kind, api.t)}</span>
226
+ </label>
227
+ <Badge kind={it.severity === 'error' ? 'error' : it.severity === 'warning' ? 'warn' : 'info'}>
228
+ {severityLabel(it.severity, api.t)}
229
+ </Badge>
230
+ <span>{it.description}</span>
231
+ {isConflict && (
232
+ <ConflictResolver
233
+ item={it}
234
+ resolution={st.resolution}
235
+ busy={busy}
236
+ t={t}
237
+ onResolve={(r) => { setResolution(it.itemId, r) }}
238
+ />
239
+ )}
240
+ </div>
241
+ );
242
+ })}
243
+ </div>
244
+ </div>
245
+
246
+ {error !== null && <Banner kind="error">{error}</Banner>}
247
+
248
+ {/* 确认导入 / 取消 */}
249
+ {phase === 'idle' && (
250
+ <div className={css.actionRow}>
251
+ <Button variant="primary" disabled={busy || summary.adopted === 0} onClick={() => { void runApply() }}>
252
+ {t('syncflow.confirmImport')}
253
+ </Button>
254
+ <Button disabled={busy} onClick={() => { void runCancel() }}>
255
+ {t('syncflow.cancel')}
256
+ </Button>
257
+ </div>
258
+ )}
259
+
260
+ {/* 执行结果 */}
261
+ {(phase === 'done' || phase === 'failed') && applyResult !== null && (
262
+ <>
263
+ <ApplyResultCard result={applyResult} busy={busy} t={t} onRollback={runRollback} />
264
+ <div className={css.actionRow} style={{ marginTop: '12px' }}>
265
+ <Button variant="primary" disabled={busy} onClick={() => { onCancel() }}>
266
+ {t('common.close')}
267
+ </Button>
268
+ </div>
269
+ </>
270
+ )}
271
+ {(phase === 'done' || phase === 'failed') && applyResult === null && (
272
+ <>
273
+ {/* R-04:回滚成功的 ok Banner 已改为 Toast(见 runRollback)。此处仅保留收尾「关闭」按钮;
274
+ 一并消除了原实现在 runApply 抛错(phase=failed 且 applyResult 仍为 null)时
275
+ 误显示「已回滚到应用前」的问题 —— 那种情况下方 error Banner 才是唯一事实。 */}
276
+ <div className={css.actionRow} style={{ marginTop: '12px' }}>
277
+ <Button variant="primary" disabled={busy} onClick={() => { onCancel() }}>
278
+ {t('common.close')}
279
+ </Button>
280
+ </div>
281
+ </>
282
+ )}
283
+ </div>
284
+ );
285
+ }
286
+
287
+ /* ---------------------------------------------------------------- 冲突内联解决 */
288
+
289
+ interface ConflictResolverProps {
290
+ item: SyncConfirmItem
291
+ resolution?: ConflictResolution
292
+ busy: boolean
293
+ t: TranslateNS<'config-manager-sync'>
294
+ onResolve: (r: ConflictResolution) => void
295
+ }
296
+
297
+ function ConflictResolver({ item, resolution, busy, t, onResolve }: ConflictResolverProps): ReactNode {
298
+ const conflict = item.conflict;
299
+ const options: { value: ConflictResolution; label: string }[] = [
300
+ { value: 'keepLocal', label: t('syncflow.conflictUseLocal') },
301
+ { value: 'useRemote', label: t('syncflow.conflictUseRemote') },
302
+ ];
303
+ return (
304
+ <div className={css.conflictItem}>
305
+ {/* 变更详情(与导入恢复向导一致:如插件「当前 1.1 vs 备份 1.6」) */}
306
+ {item.detail !== undefined && item.detail !== '' && (
307
+ <pre className={css.conflictDetail}>{item.detail}</pre>
308
+ )}
309
+ {conflict?.diff !== undefined && (
310
+ <details className={css.conflictDetail}>
311
+ <summary>{t('syncflow.diff')}</summary>
312
+ <pre className={css.diffScroll}>{conflict.diff}</pre>
313
+ </details>
314
+ )}
315
+ {/* 与导入恢复向导 ConflictList 相同的两项单选(保留当前 / 使用备份) */}
316
+ <div className={css.conflictOptions}>
317
+ {options.map((opt) => (
318
+ <label key={opt.value} className={css.radioLabel}>
319
+ <input
320
+ type="radio"
321
+ name={`sync-conflict-${item.itemId}`}
322
+ checked={resolution === opt.value}
323
+ disabled={busy}
324
+ onChange={() => { onResolve(opt.value) }}
325
+ />
326
+ <span>{opt.label}</span>
327
+ </label>
328
+ ))}
329
+ </div>
330
+ </div>
331
+ );
332
+ }
333
+
334
+ /* ---------------------------------------------------------------- 执行结果 */
335
+
336
+ interface ApplyResultCardProps {
337
+ result: ApplyItemsResponse
338
+ busy: boolean
339
+ t: TranslateNS<'config-manager-sync'>
340
+ onRollback: (restoreId: string) => Promise<void>
341
+ }
342
+
343
+ function ApplyResultCard({ result, busy, t, onRollback }: ApplyResultCardProps): ReactNode {
344
+ const ok = result.ok;
345
+ return (
346
+ <>
347
+ {/* R-05:成功分支已改走 Toast(见 runApply);此处只保留失败 Banner ——
348
+ K-06 要求失败后用户停留本卡阅读 warnings 明细并决定是否回滚,
349
+ 自动消失的 Toast 无法承载这个决策入口。 */}
350
+ {!ok && <Banner kind="error">{t('syncflow.importFailed')}</Banner>}
351
+ {result.needsRestart && <Banner kind="warn">{t('syncflow.needsRestart')}</Banner>}
352
+ {result.applied.length > 0 && (
353
+ <div>
354
+ <span className={css.fieldLabel}>{t('syncflow.importedSections')}</span>
355
+ <div className={css.statRow}>
356
+ {result.applied.map((sid) => <Badge key={sid} kind="ok">{sid}</Badge>)}
357
+ </div>
358
+ </div>
359
+ )}
360
+ {result.warnings.length > 0 && (
361
+ <div>
362
+ <span className={css.fieldLabel}>{t('syncflow.importWarnings')}</span>
363
+ <ul className={css.warnList}>
364
+ {result.warnings.map((w, i) => <li key={i}>{w}</li>)}
365
+ </ul>
366
+ </div>
367
+ )}
368
+ {result.restoreId !== '' && (
369
+ <Button
370
+ variant="danger"
371
+ disabled={busy}
372
+ onClick={() => { void onRollback(result.restoreId) }}
373
+ >
374
+ {busy ? <Spinner label={t('syncflow.rollingBack')} /> : t('syncflow.rollback')}
375
+ </Button>
376
+ )}
377
+ </>
378
+ );
379
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * P2b SyncHistoryView 纯函数测试:projectHistoryRows / formatDateTime。
3
+ */
4
+ import test from 'node:test';
5
+ import assert from 'node:assert/strict';
6
+
7
+ import { projectHistoryRows, formatDateTime } from './history-model.ts';
8
+ import type { SnapshotHistoryEntry } from './history-model.ts';
9
+
10
+ const e = (id: string, createdAt: string, sectionCount = 1, reviewCount = 0): SnapshotHistoryEntry => ({
11
+ id, createdAt, sectionCount, reviewCount,
12
+ });
13
+
14
+ test('projectHistoryRows:按 createdAt 倒序', () => {
15
+ const rows = projectHistoryRows([
16
+ e('a', '2026-08-17T10:00:00.000Z'),
17
+ e('b', '2026-08-17T12:00:00.000Z'),
18
+ e('c', '2026-08-17T11:00:00.000Z'),
19
+ ]);
20
+ assert.equal(rows[0]!.id, 'b');
21
+ assert.equal(rows[1]!.id, 'c');
22
+ assert.equal(rows[2]!.id, 'a');
23
+ });
24
+
25
+ test('projectHistoryRows:空集合 → 空', () => {
26
+ assert.deepEqual(projectHistoryRows([]), []);
27
+ });
28
+
29
+ test('formatDateTime:合法 ISO → 本地可读格式', () => {
30
+ const s = formatDateTime('2026-08-17T10:30:00.000Z');
31
+ assert.match(s, /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/);
32
+ });
33
+
34
+ test('formatDateTime:空串 → 「—」', () => {
35
+ assert.equal(formatDateTime(''), '—');
36
+ });
37
+
38
+ test('formatDateTime:非法 ISO → 原样返回', () => {
39
+ assert.equal(formatDateTime('not-a-date'), 'not-a-date');
40
+ });
@@ -0,0 +1,220 @@
1
+ /**
2
+ * 同步历史视图(方案 A):列出本地祖先快照目录(kind=apply)+ 自动同步执行记录
3
+ * (kind=autosync)。自动同步行显示时间/方向/状态/跳过冲突/应用分区,点开可看被跳过
4
+ * 冲突分区明细。
5
+ *
6
+ * 数据获取:GET /sync/history → { entries: SyncHistoryEntry[] }(按 createdAt 倒序合并)。
7
+ * 纯函数投影在 ./history-model.ts(node --test 可测),本组件只做装配。
8
+ */
9
+ import { useEffect, useMemo, useState } from 'react'
10
+ import type { ReactNode } from 'react'
11
+
12
+ import { Badge, Card, SectionTitle, Spinner } from '../common/ui.tsx'
13
+ import { ErrorBanner } from '../common/ErrorBanner.tsx'
14
+ import type { SyncApi, SyncHistoryEntry, AutosyncHistoryEntry } from './sync-api.ts'
15
+ import {
16
+ formatDateTime, formatDateTimeFull, midEllipsis, projectAutosyncEntry,
17
+ projectSyncHistoryEntries, summarizeSyncHistory,
18
+ } from './history-model.ts'
19
+ import type { SnapshotHistoryEntry } from './history-model.ts'
20
+ import type { TranslateNS } from '../client-types.ts'
21
+ import css from '../config-manager.module.css'
22
+
23
+ /** 通道徽章:git → GitHub,webdav → WebDAV;未知/缺失不渲染。 */
24
+ function ChannelBadge({ transport, t }: { transport?: string; t: TranslateNS<'config-manager-sync'> }): ReactNode {
25
+ if (transport === 'git') return <Badge kind="info">{t('history.channelGit')}</Badge>
26
+ if (transport === 'webdav') return <Badge kind="info">{t('history.channelWebdav')}</Badge>
27
+ return null
28
+ }
29
+
30
+ export interface SyncHistoryViewProps {
31
+ api: SyncApi
32
+ t: TranslateNS<'config-manager-sync'>
33
+ }
34
+
35
+ export function SyncHistoryView(props: SyncHistoryViewProps): ReactNode {
36
+ const { api, t } = props;
37
+ const [loading, setLoading] = useState(true);
38
+ const [error, setError] = useState<string | null>(null);
39
+ const [entries, setEntries] = useState<SyncHistoryEntry[]>([]);
40
+ /** 重试计数(错误态点「重试」递增 → 重新加载) */
41
+ const [reloadKey, setReloadKey] = useState(0);
42
+
43
+ useEffect(() => {
44
+ let cancelled = false;
45
+ setLoading(true);
46
+ (async () => {
47
+ try {
48
+ const data = await api.history();
49
+ if (!cancelled) {
50
+ setEntries(data.entries);
51
+ setError(null);
52
+ setLoading(false);
53
+ }
54
+ } catch (err) {
55
+ if (!cancelled) {
56
+ setError(err instanceof Error ? err.message : String(err));
57
+ setLoading(false);
58
+ }
59
+ }
60
+ })();
61
+ return () => { cancelled = true; };
62
+ }, [api, reloadKey]);
63
+
64
+ const rows = useMemo(() => projectSyncHistoryEntries(entries), [entries]);
65
+ const stats = useMemo(() => summarizeSyncHistory(rows), [rows]);
66
+ // 快照类条目(兼容旧投影;仅统计展示)
67
+ const snapshotRows = useMemo<SnapshotHistoryEntry[]>(
68
+ () => rows
69
+ .filter((r) => r.kind === 'apply' || r.kind === 'push' || r.kind === 'pull' || r.kind === 'rollback')
70
+ .map((r) => ({
71
+ id: r.id,
72
+ createdAt: r.createdAt,
73
+ sectionCount: r.sectionCount ?? 0,
74
+ reviewCount: r.reviewCount ?? 0,
75
+ })),
76
+ [rows],
77
+ );
78
+
79
+ if (loading) return <Spinner label={t('common.loading')} />;
80
+ if (error) {
81
+ return (
82
+ <ErrorBanner
83
+ error={error}
84
+ onRetry={() => { setReloadKey((k) => k + 1) }}
85
+ retrying={loading}
86
+ t={api.t}
87
+ />
88
+ );
89
+ }
90
+ if (rows.length === 0) {
91
+ return (
92
+ <Card>
93
+ <strong>{t('history.empty')}</strong>
94
+ <p>{t('history.emptyHint')}</p>
95
+ </Card>
96
+ );
97
+ }
98
+
99
+ return (
100
+ <Card>
101
+ <SectionTitle title={`${t('history.title')}(${rows.length})`} />
102
+ {/* F:头部统计摘要(先扫结论);失败/跳过仅在存在时出现,并给语义色 */}
103
+ <div className={css.statRow} aria-label={t('history.stats.summary')}>
104
+ <Badge kind="info">{t('history.stats.total', { count: String(stats.total) })}</Badge>
105
+ <Badge kind="info">{t('history.stats.snapshots', { count: String(stats.snapshots) })}</Badge>
106
+ <Badge kind="info">{t('history.stats.autosync', { count: String(stats.autosync) })}</Badge>
107
+ {stats.failed > 0 && <Badge kind="error">{t('history.stats.failed', { count: String(stats.failed) })}</Badge>}
108
+ {stats.skipped > 0 && <Badge kind="warn">{t('history.stats.skipped', { count: String(stats.skipped) })}</Badge>}
109
+ </div>
110
+ {/* A:改用设计系统数据表(.tableWrap > .tableScroll 限高内滚 + .dataTable/.tableFixed/.tableCompact) */}
111
+ <div className={css.tableWrap}>
112
+ <div className={css.tableScroll}>
113
+ <table className={`${css.dataTable} ${css.tableFixed} ${css.tableCompact}`}>
114
+ <thead>
115
+ <tr>
116
+ {/* 时间列 116px(非 92px):11px 等宽下 "YYYY-MM-DD HH:mm" 实测 96.8px,
117
+ 加 .tableCompact 的左右各 8px padding 需 112.8px;92px 会把时间截成
118
+ 「2026-09-01…」(浏览器实测截图确认),反而比改前更不可读。 */}
119
+ <th style={{ width: 116 }}>{t('history.colTime')}</th>
120
+ <th style={{ width: 96 }}>{t('history.colKind')}</th>
121
+ <th>{t('history.colDetail')}</th>
122
+ </tr>
123
+ </thead>
124
+ <tbody>
125
+ {rows.map((r) => {
126
+ if (r.kind === 'autosync' && r.autosync !== undefined) {
127
+ return <AutosyncRow key={r.id} entry={r.autosync} t={t} />;
128
+ }
129
+ const snap = snapshotRows.find((s) => s.id === r.id);
130
+ return (
131
+ <tr key={r.id}>
132
+ {/* B:等宽 11px + 固定列宽 → 单行不换行;title 给完整本地时间(含秒) */}
133
+ <td className={css.dim} title={formatDateTimeFull(r.createdAt)}>
134
+ <span className={`${css.mono}`} style={{ fontSize: '11px' }}>{formatDateTime(r.createdAt)}</span>
135
+ </td>
136
+ <td><Badge kind="info">{t('history.kindSnapshot')}</Badge></td>
137
+ <td>
138
+ <div className={css.cellMain}>
139
+ {/* C:UUID 中段省略(保留头尾,尾部才是区分信息),title 保留全文 */}
140
+ <span className={`${css.cellTitle} ${css.mono}`} title={r.id}>{midEllipsis(r.id)}</span>
141
+ <span className={css.cellMeta}>
142
+ <ChannelBadge transport={r.transport} t={t} />
143
+ {snap !== undefined && <>{snap.sectionCount} {t('history.sectionCount')}</>}
144
+ </span>
145
+ </div>
146
+ </td>
147
+ </tr>
148
+ );
149
+ })}
150
+ </tbody>
151
+ </table>
152
+ </div>
153
+ </div>
154
+ </Card>
155
+ );
156
+ }
157
+
158
+ /* ---------------------------------------------------------------- 自动同步行 */
159
+
160
+ interface AutosyncRowProps {
161
+ entry: AutosyncHistoryEntry
162
+ t: TranslateNS<'config-manager-sync'>
163
+ }
164
+
165
+ function AutosyncRow({ entry, t }: AutosyncRowProps): ReactNode {
166
+ const row = projectAutosyncEntry(entry);
167
+ return (
168
+ <tr>
169
+ {/* B:与快照行一致的时间呈现(等宽 11px 单行 + title 完整本地时间) */}
170
+ <td className={css.dim} title={formatDateTimeFull(row.createdAt)}>
171
+ <span className={`${css.mono}`} style={{ fontSize: '11px' }}>{formatDateTime(row.createdAt)}</span>
172
+ </td>
173
+ {/* D:类型徽章按实况给语义色(success=ok / failed=error / skipped、partial=warn) */}
174
+ <td><Badge kind={row.badgeKind}>{t('history.kindAutosync')}</Badge></td>
175
+ <td>
176
+ <div className={css.cellMain}>
177
+ {/* E:主行「方向 + 状态」徽章(原摘要串里的跳过原因下沉到第二行小字) */}
178
+ <span className={css.cellTitle}>
179
+ <ChannelBadge transport={entry.transport} t={t} />
180
+ {' '}
181
+ <Badge kind="info">{row.direction}</Badge>
182
+ {' '}
183
+ <Badge kind={row.badgeKind}>{row.status}</Badge>
184
+ {entry.pushedSnapshotId !== undefined && <>{' · '}{t('history.autosyncPush')} <span className={css.mono} title={entry.pushedSnapshotId}>{midEllipsis(entry.pushedSnapshotId)}</span></>}
185
+ {entry.pulledSnapshotId !== undefined && <>{' · '}{t('history.autosyncPull')} <span className={css.mono} title={entry.pulledSnapshotId}>{midEllipsis(entry.pulledSnapshotId)}</span></>}
186
+ </span>
187
+ {/* E:第二行小字——跳过原因 / 错误(信息保留,但不再挤在主行里) */}
188
+ {row.skipReasonText !== undefined && <span className={css.hint}>{row.skipReasonText}</span>}
189
+ {row.error !== undefined && <span className={css.hint} title={row.error}>{t('history.autosyncError', { error: '' })}{row.error}</span>}
190
+ </div>
191
+ {row.hasDetail && (
192
+ <details>
193
+ <summary>{t('history.detail')}</summary>
194
+ <div className={css.reportList}>
195
+ {row.conflictedSections !== undefined && row.conflictedSections.length > 0 && (
196
+ <div>
197
+ <span className={css.fieldLabel}>{t('history.autosyncConflicted', { sections: '' })}</span>
198
+ <div className={css.statRow}>
199
+ {row.conflictedSections.map((sid) => <Badge key={sid} kind="warn">{sid}</Badge>)}
200
+ </div>
201
+ </div>
202
+ )}
203
+ {row.appliedSections !== undefined && row.appliedSections.length > 0 && (
204
+ <div>
205
+ <span className={css.fieldLabel}>{t('history.autosyncApplied', { sections: '' })}</span>
206
+ <div className={css.statRow}>
207
+ {row.appliedSections.map((sid) => <Badge key={sid} kind="ok">{sid}</Badge>)}
208
+ </div>
209
+ </div>
210
+ )}
211
+ {row.error !== undefined && (
212
+ <div><span className={css.fieldLabel}>{t('history.autosyncError', { error: '' })}</span>{row.error}</div>
213
+ )}
214
+ </div>
215
+ </details>
216
+ )}
217
+ </td>
218
+ </tr>
219
+ );
220
+ }