@purposeinplay/payload-ai-translate 0.1.0

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 (301) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +714 -0
  3. package/dist/alerts-collection.d.ts +21 -0
  4. package/dist/alerts-collection.js +159 -0
  5. package/dist/api.d.ts +4 -0
  6. package/dist/api.js +918 -0
  7. package/dist/bulk-translate-batches-collection.d.ts +29 -0
  8. package/dist/bulk-translate-batches-collection.js +404 -0
  9. package/dist/bulk-translate-units-collection.d.ts +35 -0
  10. package/dist/bulk-translate-units-collection.js +310 -0
  11. package/dist/client/estimated-cost-cell.d.ts +6 -0
  12. package/dist/client/estimated-cost-cell.js +12 -0
  13. package/dist/client/excluded-fields-field.d.ts +45 -0
  14. package/dist/client/excluded-fields-field.js +553 -0
  15. package/dist/client/field-translate-button.d.ts +6 -0
  16. package/dist/client/field-translate-button.js +199 -0
  17. package/dist/client/index.d.ts +6 -0
  18. package/dist/client/index.js +6 -0
  19. package/dist/client/lib/use-global-kill-switches.d.ts +20 -0
  20. package/dist/client/lib/use-global-kill-switches.js +58 -0
  21. package/dist/client/translate-button.d.ts +2 -0
  22. package/dist/client/translate-button.js +228 -0
  23. package/dist/client/translate-modal.d.ts +16 -0
  24. package/dist/client/translate-modal.js +549 -0
  25. package/dist/client/translation-progress.d.ts +10 -0
  26. package/dist/client/translation-progress.js +297 -0
  27. package/dist/components/TranslationNavGroup.d.ts +45 -0
  28. package/dist/components/TranslationNavGroup.js +104 -0
  29. package/dist/defaults.d.ts +11 -0
  30. package/dist/defaults.js +16 -0
  31. package/dist/endpoints/client-config.d.ts +44 -0
  32. package/dist/endpoints/client-config.js +145 -0
  33. package/dist/endpoints/estimate.d.ts +5 -0
  34. package/dist/endpoints/estimate.js +237 -0
  35. package/dist/endpoints/progress.d.ts +2 -0
  36. package/dist/endpoints/progress.js +314 -0
  37. package/dist/endpoints/translate.d.ts +11 -0
  38. package/dist/endpoints/translate.js +376 -0
  39. package/dist/endpoints/translation-hub/_helpers.d.ts +140 -0
  40. package/dist/endpoints/translation-hub/_helpers.js +297 -0
  41. package/dist/endpoints/translation-hub/active.d.ts +21 -0
  42. package/dist/endpoints/translation-hub/active.js +220 -0
  43. package/dist/endpoints/translation-hub/cancel.d.ts +22 -0
  44. package/dist/endpoints/translation-hub/cancel.js +233 -0
  45. package/dist/endpoints/translation-hub/enqueue.d.ts +70 -0
  46. package/dist/endpoints/translation-hub/enqueue.js +529 -0
  47. package/dist/endpoints/translation-hub/failures.d.ts +12 -0
  48. package/dist/endpoints/translation-hub/failures.js +67 -0
  49. package/dist/endpoints/translation-hub/force-reset.d.ts +20 -0
  50. package/dist/endpoints/translation-hub/force-reset.js +144 -0
  51. package/dist/endpoints/translation-hub/index.d.ts +21 -0
  52. package/dist/endpoints/translation-hub/index.js +20 -0
  53. package/dist/endpoints/translation-hub/list.d.ts +40 -0
  54. package/dist/endpoints/translation-hub/list.js +182 -0
  55. package/dist/endpoints/translation-hub/preflight.d.ts +19 -0
  56. package/dist/endpoints/translation-hub/preflight.js +141 -0
  57. package/dist/endpoints/translation-hub/retry-failed.d.ts +38 -0
  58. package/dist/endpoints/translation-hub/retry-failed.js +235 -0
  59. package/dist/endpoints/translation-hub/revert.d.ts +88 -0
  60. package/dist/endpoints/translation-hub/revert.js +405 -0
  61. package/dist/endpoints/translation-hub/status.d.ts +45 -0
  62. package/dist/endpoints/translation-hub/status.js +391 -0
  63. package/dist/endpoints/translation-hub/usage-summary.d.ts +114 -0
  64. package/dist/endpoints/translation-hub/usage-summary.js +481 -0
  65. package/dist/exports/client.d.ts +6 -0
  66. package/dist/exports/client.js +6 -0
  67. package/dist/exports/components.d.ts +6 -0
  68. package/dist/exports/components.js +5 -0
  69. package/dist/exports/index.d.ts +8 -0
  70. package/dist/exports/index.js +7 -0
  71. package/dist/exports/providers.d.ts +9 -0
  72. package/dist/exports/providers.js +5 -0
  73. package/dist/exports/views-client.d.ts +23 -0
  74. package/dist/exports/views-client.js +22 -0
  75. package/dist/exports/views.d.ts +30 -0
  76. package/dist/exports/views.js +29 -0
  77. package/dist/hooks/after-change-global.d.ts +4 -0
  78. package/dist/hooks/after-change-global.js +109 -0
  79. package/dist/hooks/after-change.d.ts +16 -0
  80. package/dist/hooks/after-change.js +205 -0
  81. package/dist/hooks/after-delete.d.ts +30 -0
  82. package/dist/hooks/after-delete.js +95 -0
  83. package/dist/index.d.ts +5 -0
  84. package/dist/index.js +5 -0
  85. package/dist/jobs-collection.d.ts +17 -0
  86. package/dist/jobs-collection.js +139 -0
  87. package/dist/lexical/classifier.d.ts +3 -0
  88. package/dist/lexical/classifier.js +108 -0
  89. package/dist/lexical/deserializer.d.ts +4 -0
  90. package/dist/lexical/deserializer.js +263 -0
  91. package/dist/lexical/placeholder-integrity.d.ts +6 -0
  92. package/dist/lexical/placeholder-integrity.js +21 -0
  93. package/dist/lexical/placeholders.d.ts +21 -0
  94. package/dist/lexical/placeholders.js +117 -0
  95. package/dist/lexical/serializer.d.ts +21 -0
  96. package/dist/lexical/serializer.js +233 -0
  97. package/dist/lexical/types.d.ts +32 -0
  98. package/dist/lexical/types.js +1 -0
  99. package/dist/lib/auth-diagnostics.d.ts +14 -0
  100. package/dist/lib/auth-diagnostics.js +19 -0
  101. package/dist/lib/batch-counts.d.ts +58 -0
  102. package/dist/lib/batch-counts.js +105 -0
  103. package/dist/lib/bulk-translate-migrations.d.ts +92 -0
  104. package/dist/lib/bulk-translate-migrations.js +153 -0
  105. package/dist/lib/coalescing-queue.d.ts +38 -0
  106. package/dist/lib/coalescing-queue.js +69 -0
  107. package/dist/lib/content-extractor.d.ts +16 -0
  108. package/dist/lib/content-extractor.js +410 -0
  109. package/dist/lib/content-hash.d.ts +1 -0
  110. package/dist/lib/content-hash.js +19 -0
  111. package/dist/lib/content-patcher.d.ts +15 -0
  112. package/dist/lib/content-patcher.js +293 -0
  113. package/dist/lib/cost-guards.d.ts +2 -0
  114. package/dist/lib/cost-guards.js +18 -0
  115. package/dist/lib/daily-spend-cap.d.ts +58 -0
  116. package/dist/lib/daily-spend-cap.js +233 -0
  117. package/dist/lib/effective-locales.d.ts +181 -0
  118. package/dist/lib/effective-locales.js +302 -0
  119. package/dist/lib/error-messages.d.ts +245 -0
  120. package/dist/lib/error-messages.js +626 -0
  121. package/dist/lib/events.d.ts +39 -0
  122. package/dist/lib/events.js +146 -0
  123. package/dist/lib/exclude-fields.d.ts +3 -0
  124. package/dist/lib/exclude-fields.js +64 -0
  125. package/dist/lib/field-breadcrumb.d.ts +31 -0
  126. package/dist/lib/field-breadcrumb.js +227 -0
  127. package/dist/lib/field-diff.d.ts +1 -0
  128. package/dist/lib/field-diff.js +25 -0
  129. package/dist/lib/field-empty.d.ts +2 -0
  130. package/dist/lib/field-empty.js +68 -0
  131. package/dist/lib/field-resolver.d.ts +3 -0
  132. package/dist/lib/field-resolver.js +164 -0
  133. package/dist/lib/group-soft-skips.d.ts +39 -0
  134. package/dist/lib/group-soft-skips.js +45 -0
  135. package/dist/lib/locale-merge.d.ts +44 -0
  136. package/dist/lib/locale-merge.js +357 -0
  137. package/dist/lib/locale-row-check.d.ts +30 -0
  138. package/dist/lib/locale-row-check.js +64 -0
  139. package/dist/lib/logger.d.ts +74 -0
  140. package/dist/lib/logger.js +97 -0
  141. package/dist/lib/manual-edit-guard.d.ts +128 -0
  142. package/dist/lib/manual-edit-guard.js +393 -0
  143. package/dist/lib/output-validation.d.ts +48 -0
  144. package/dist/lib/output-validation.js +148 -0
  145. package/dist/lib/payload-read.d.ts +16 -0
  146. package/dist/lib/payload-read.js +51 -0
  147. package/dist/lib/per-doc-claim.d.ts +90 -0
  148. package/dist/lib/per-doc-claim.js +140 -0
  149. package/dist/lib/per-doc-lock.d.ts +94 -0
  150. package/dist/lib/per-doc-lock.js +119 -0
  151. package/dist/lib/persist-usage.d.ts +91 -0
  152. package/dist/lib/persist-usage.js +116 -0
  153. package/dist/lib/progress-store.d.ts +103 -0
  154. package/dist/lib/progress-store.js +314 -0
  155. package/dist/lib/rate-limiter.d.ts +3 -0
  156. package/dist/lib/rate-limiter.js +53 -0
  157. package/dist/lib/snapshot-select.d.ts +43 -0
  158. package/dist/lib/snapshot-select.js +108 -0
  159. package/dist/lib/translate-prompt.d.ts +31 -0
  160. package/dist/lib/translate-prompt.js +66 -0
  161. package/dist/lib/translation-token-bucket.d.ts +57 -0
  162. package/dist/lib/translation-token-bucket.js +365 -0
  163. package/dist/lib/truncate-source-value.d.ts +1 -0
  164. package/dist/lib/truncate-source-value.js +27 -0
  165. package/dist/manual-edit-collection.d.ts +22 -0
  166. package/dist/manual-edit-collection.js +124 -0
  167. package/dist/plugin.d.ts +3 -0
  168. package/dist/plugin.js +934 -0
  169. package/dist/providers/ai-sdk-adapter.d.ts +35 -0
  170. package/dist/providers/ai-sdk-adapter.js +100 -0
  171. package/dist/providers/anthropic.d.ts +31 -0
  172. package/dist/providers/anthropic.js +66 -0
  173. package/dist/providers/custom.d.ts +36 -0
  174. package/dist/providers/custom.js +24 -0
  175. package/dist/providers/gemini.d.ts +20 -0
  176. package/dist/providers/gemini.js +48 -0
  177. package/dist/providers/mock.d.ts +2 -0
  178. package/dist/providers/mock.js +29 -0
  179. package/dist/providers/openai.d.ts +28 -0
  180. package/dist/providers/openai.js +69 -0
  181. package/dist/settings-global.d.ts +74 -0
  182. package/dist/settings-global.js +216 -0
  183. package/dist/tasks/bulk-translate-coordinator.d.ts +115 -0
  184. package/dist/tasks/bulk-translate-coordinator.js +708 -0
  185. package/dist/tasks/bulk-translate-doc-task.d.ts +142 -0
  186. package/dist/tasks/bulk-translate-doc-task.js +1000 -0
  187. package/dist/tasks/bulk-translate-janitor.d.ts +87 -0
  188. package/dist/tasks/bulk-translate-janitor.js +311 -0
  189. package/dist/tasks/translate-job-task.d.ts +51 -0
  190. package/dist/tasks/translate-job-task.js +154 -0
  191. package/dist/translate.d.ts +113 -0
  192. package/dist/translate.js +911 -0
  193. package/dist/translation-daily-spend-collection.d.ts +24 -0
  194. package/dist/translation-daily-spend-collection.js +133 -0
  195. package/dist/translation-rate-limits-collection.d.ts +30 -0
  196. package/dist/translation-rate-limits-collection.js +144 -0
  197. package/dist/types.d.ts +672 -0
  198. package/dist/types.js +1 -0
  199. package/dist/usage-collection.d.ts +14 -0
  200. package/dist/usage-collection.js +377 -0
  201. package/dist/views/BulkRunsHub/BatchRow.d.ts +32 -0
  202. package/dist/views/BulkRunsHub/BatchRow.js +1222 -0
  203. package/dist/views/BulkRunsHub/BucketRow.d.ts +62 -0
  204. package/dist/views/BulkRunsHub/BucketRow.js +982 -0
  205. package/dist/views/BulkRunsHub/BulkRunsHub.client.d.ts +18 -0
  206. package/dist/views/BulkRunsHub/BulkRunsHub.client.js +331 -0
  207. package/dist/views/BulkRunsHub/EmptyState.d.ts +6 -0
  208. package/dist/views/BulkRunsHub/EmptyState.js +64 -0
  209. package/dist/views/BulkRunsHub/FilterBar.d.ts +16 -0
  210. package/dist/views/BulkRunsHub/FilterBar.js +284 -0
  211. package/dist/views/BulkRunsHub/InFlightBanner.d.ts +14 -0
  212. package/dist/views/BulkRunsHub/InFlightBanner.js +59 -0
  213. package/dist/views/BulkRunsHub/StatusBadge.d.ts +64 -0
  214. package/dist/views/BulkRunsHub/StatusBadge.js +248 -0
  215. package/dist/views/BulkRunsHub/SummaryStrip.d.ts +22 -0
  216. package/dist/views/BulkRunsHub/SummaryStrip.js +249 -0
  217. package/dist/views/BulkRunsHub/bucket-grouping.d.ts +200 -0
  218. package/dist/views/BulkRunsHub/bucket-grouping.js +344 -0
  219. package/dist/views/BulkRunsHub/bucketFailureSummary.d.ts +9 -0
  220. package/dist/views/BulkRunsHub/bucketFailureSummary.js +36 -0
  221. package/dist/views/BulkRunsHub/dedupedStatusFetch.d.ts +5 -0
  222. package/dist/views/BulkRunsHub/dedupedStatusFetch.js +45 -0
  223. package/dist/views/BulkRunsHub/index.d.ts +17 -0
  224. package/dist/views/BulkRunsHub/index.js +80 -0
  225. package/dist/views/BulkRunsHub/urlFilters.d.ts +14 -0
  226. package/dist/views/BulkRunsHub/urlFilters.js +50 -0
  227. package/dist/views/BulkRunsHub/useBulkRunsList.d.ts +26 -0
  228. package/dist/views/BulkRunsHub/useBulkRunsList.js +204 -0
  229. package/dist/views/BulkRunsHub/useUrlFilters.d.ts +10 -0
  230. package/dist/views/BulkRunsHub/useUrlFilters.js +88 -0
  231. package/dist/views/TranslationHub/ActiveJobs.d.ts +6 -0
  232. package/dist/views/TranslationHub/ActiveJobs.js +320 -0
  233. package/dist/views/TranslationHub/AdvancedPanel.d.ts +17 -0
  234. package/dist/views/TranslationHub/AdvancedPanel.js +996 -0
  235. package/dist/views/TranslationHub/AlertBanner.d.ts +6 -0
  236. package/dist/views/TranslationHub/AlertBanner.js +568 -0
  237. package/dist/views/TranslationHub/AuditPanel.d.ts +6 -0
  238. package/dist/views/TranslationHub/AuditPanel.helpers.d.ts +44 -0
  239. package/dist/views/TranslationHub/AuditPanel.helpers.js +71 -0
  240. package/dist/views/TranslationHub/AuditPanel.js +1367 -0
  241. package/dist/views/TranslationHub/BulkTranslate.types.d.ts +242 -0
  242. package/dist/views/TranslationHub/BulkTranslate.types.js +36 -0
  243. package/dist/views/TranslationHub/BulkTranslateFailureDrawer.d.ts +19 -0
  244. package/dist/views/TranslationHub/BulkTranslateFailureDrawer.js +332 -0
  245. package/dist/views/TranslationHub/BulkTranslateMonitor.d.ts +28 -0
  246. package/dist/views/TranslationHub/BulkTranslateMonitor.js +305 -0
  247. package/dist/views/TranslationHub/BulkTranslateNarrowViewportBanner.d.ts +3 -0
  248. package/dist/views/TranslationHub/BulkTranslateNarrowViewportBanner.js +42 -0
  249. package/dist/views/TranslationHub/BulkTranslatePostEnqueueTransition.d.ts +26 -0
  250. package/dist/views/TranslationHub/BulkTranslatePostEnqueueTransition.js +95 -0
  251. package/dist/views/TranslationHub/BulkTranslatePreflightModal.d.ts +22 -0
  252. package/dist/views/TranslationHub/BulkTranslatePreflightModal.js +879 -0
  253. package/dist/views/TranslationHub/BulkTranslateTerminalCard.d.ts +29 -0
  254. package/dist/views/TranslationHub/BulkTranslateTerminalCard.js +445 -0
  255. package/dist/views/TranslationHub/BulkTranslateTrigger.d.ts +66 -0
  256. package/dist/views/TranslationHub/BulkTranslateTrigger.js +161 -0
  257. package/dist/views/TranslationHub/EditorRecentRunsPanel.d.ts +33 -0
  258. package/dist/views/TranslationHub/EditorRecentRunsPanel.js +290 -0
  259. package/dist/views/TranslationHub/Hub.client.d.ts +74 -0
  260. package/dist/views/TranslationHub/Hub.client.js +357 -0
  261. package/dist/views/TranslationHub/ModelCombobox.d.ts +14 -0
  262. package/dist/views/TranslationHub/ModelCombobox.js +415 -0
  263. package/dist/views/TranslationHub/PerCollectionConfig.d.ts +10 -0
  264. package/dist/views/TranslationHub/PerCollectionConfig.helpers.d.ts +16 -0
  265. package/dist/views/TranslationHub/PerCollectionConfig.helpers.js +19 -0
  266. package/dist/views/TranslationHub/PerCollectionConfig.js +759 -0
  267. package/dist/views/TranslationHub/SettingsRail.d.ts +11 -0
  268. package/dist/views/TranslationHub/SettingsRail.js +382 -0
  269. package/dist/views/TranslationHub/StatusStrip.d.ts +6 -0
  270. package/dist/views/TranslationHub/StatusStrip.js +451 -0
  271. package/dist/views/TranslationHub/UsageTable.d.ts +6 -0
  272. package/dist/views/TranslationHub/UsageTable.helpers.d.ts +69 -0
  273. package/dist/views/TranslationHub/UsageTable.helpers.js +49 -0
  274. package/dist/views/TranslationHub/UsageTable.js +1240 -0
  275. package/dist/views/TranslationHub/alertGrouping.d.ts +70 -0
  276. package/dist/views/TranslationHub/alertGrouping.js +99 -0
  277. package/dist/views/TranslationHub/index.d.ts +20 -0
  278. package/dist/views/TranslationHub/index.js +109 -0
  279. package/dist/views/TranslationHub/tabNavigation.d.ts +53 -0
  280. package/dist/views/TranslationHub/tabNavigation.js +74 -0
  281. package/dist/views/TranslationHub/terminalBannerVisibility.d.ts +33 -0
  282. package/dist/views/TranslationHub/terminalBannerVisibility.js +124 -0
  283. package/dist/views/TranslationHub/useBulkTranslateActive.d.ts +49 -0
  284. package/dist/views/TranslationHub/useBulkTranslateActive.js +251 -0
  285. package/dist/views/TranslationHub/useFocusTrap.d.ts +6 -0
  286. package/dist/views/TranslationHub/useFocusTrap.js +81 -0
  287. package/dist/views/TranslationHub/useTranslationHubUsageSummary.d.ts +77 -0
  288. package/dist/views/TranslationHub/useTranslationHubUsageSummary.js +267 -0
  289. package/dist/views/shared/EditorError.d.ts +97 -0
  290. package/dist/views/shared/EditorError.js +205 -0
  291. package/dist/views/shared/ModelCell.d.ts +18 -0
  292. package/dist/views/shared/ModelCell.js +31 -0
  293. package/dist/views/shared/docHref.d.ts +16 -0
  294. package/dist/views/shared/docHref.js +26 -0
  295. package/dist/views/shared/fetch-error-body.d.ts +25 -0
  296. package/dist/views/shared/fetch-error-body.js +42 -0
  297. package/dist/views/shared/filterPillStyle.d.ts +35 -0
  298. package/dist/views/shared/filterPillStyle.js +40 -0
  299. package/dist/views/shared/format.d.ts +75 -0
  300. package/dist/views/shared/format.js +131 -0
  301. package/package.json +141 -0
@@ -0,0 +1,982 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useEffect, useState } from 'react';
4
+ import { categorizeFailure } from '../../lib/error-messages.js';
5
+ import { docHref, globalHref } from '../shared/docHref.js';
6
+ import { EditorError } from '../shared/EditorError.js';
7
+ import { formatCost, formatDuration } from '../shared/format.js';
8
+ import { fmtBucketAiActive, fmtBucketQueued as fmtBucketQueuedHelper, getBucketCountForFilter } from './bucket-grouping.js';
9
+ import { summarizeBucketFailureTypes } from './bucketFailureSummary.js';
10
+ // ---------------------------------------------------------------------------
11
+ // Timestamp formatting — module-scoped helper. Duration and cost are
12
+ // rendered via the shared `formatDuration` / `formatCost` helpers in
13
+ // `views/shared/format.ts` (NEW-17 / NEW-18 / NEW-10).
14
+ // ---------------------------------------------------------------------------
15
+ // ROUND5-2 (v1.2.6): predicate extracted from ROUND3-7's row-summary
16
+ // fix. The deep `LocaleDetailRow` panel was building an `Open →` link
17
+ // from `documentId` without re-running ROUND3-7's guard, so cancel-
18
+ // mid-flight units (which can land with `documentId === null` or
19
+ // the literal string `'null'`) produced `/admin/collections/posts/null`
20
+ // links — the same broken UX ROUND3-7 was meant to prevent, just
21
+ // behind one more click. Single source of truth so any new render
22
+ // site can re-use it.
23
+ function isOpenableDocId(id) {
24
+ return id != null && id !== 'null' && id !== '';
25
+ }
26
+ function fmtTimestamp(iso) {
27
+ if (!iso) return '—';
28
+ try {
29
+ return new Date(iso).toLocaleString(undefined, {
30
+ month: 'short',
31
+ day: 'numeric',
32
+ hour: '2-digit',
33
+ minute: '2-digit',
34
+ second: '2-digit'
35
+ });
36
+ } catch {
37
+ return '—';
38
+ }
39
+ }
40
+ /**
41
+ * Tree-grouped DrillDown row component. One BucketRow per collection /
42
+ * global. Default expansion state controlled by parent (failed buckets
43
+ * open, all-success collapsed).
44
+ *
45
+ * Per UX design: bucket header shows succeeded + failed counts, top-2
46
+ * failure codes (when there are failures), and a "Retry N failed" CTA
47
+ * that maps to the existing retry-failed endpoint with a `collection`
48
+ * filter. NO per-bucket cost/duration (cost belongs in Audit & Cost tab;
49
+ * duration is meaningful at the batch level only).
50
+ *
51
+ * Per UI design: elevation-50 tint on the bucket header, red as the only
52
+ * saturated color on screen (success pills muted by default, failure
53
+ * pills saturated), 3px error-500 left border on failed doc rows.
54
+ */ const BUCKET_HEADER_STYLE = {
55
+ background: 'var(--theme-elevation-50, #f1f5f9)',
56
+ borderBottom: '1px solid var(--theme-elevation-100, #e5e7eb)',
57
+ padding: '0.625rem 1rem',
58
+ fontSize: '0.8125rem',
59
+ fontWeight: 600,
60
+ color: 'var(--theme-elevation-1000)',
61
+ cursor: 'pointer',
62
+ display: 'flex',
63
+ alignItems: 'center',
64
+ gap: '0.5rem',
65
+ userSelect: 'none'
66
+ };
67
+ const CHEVRON_STYLE = {
68
+ color: 'var(--theme-elevation-500)',
69
+ fontSize: '0.75rem',
70
+ width: '0.75rem',
71
+ display: 'inline-block',
72
+ transition: 'transform 120ms ease'
73
+ };
74
+ const PILL_STYLE_BASE = {
75
+ fontSize: '0.6875rem',
76
+ fontWeight: 500,
77
+ display: 'inline-flex',
78
+ alignItems: 'center',
79
+ gap: '0.25rem'
80
+ };
81
+ const RETRY_BUTTON_STYLE = {
82
+ border: '1px solid var(--theme-error-500, #b91c1c)',
83
+ color: 'var(--theme-error-500, #b91c1c)',
84
+ background: 'transparent',
85
+ borderRadius: '4px',
86
+ padding: '0.1875rem 0.625rem',
87
+ fontSize: '0.6875rem',
88
+ fontWeight: 500,
89
+ cursor: 'pointer',
90
+ marginLeft: '1rem'
91
+ };
92
+ const GLOBAL_BADGE_STYLE = {
93
+ fontSize: '0.6875rem',
94
+ color: 'var(--theme-elevation-700)',
95
+ border: '1px solid var(--theme-elevation-200)',
96
+ borderRadius: '3px',
97
+ padding: '0 0.3rem',
98
+ marginLeft: '0.375rem',
99
+ fontWeight: 400
100
+ };
101
+ export const BucketRow = ({ bucket, basePath, batchId: _batchId, batchStatus, activeStatusFilter, initialExpanded, isRetrying, onRetryBucket, onExpandedChange, retryingDocKey, onRetryDoc, onLoadBucketDocs, hasMoreInBucket, isLoadingMoreInBucket })=>{
102
+ const [expanded, setExpanded] = useState(initialExpanded);
103
+ const [loading, setLoading] = useState(false);
104
+ const [loadError, setLoadError] = useState(null);
105
+ const hasFailures = bucket.failedCount > 0;
106
+ const hasInflight = bucket.pendingCount + bucket.runningCount > 0;
107
+ // v1.2.7: filter-aware "how many units match the active filter live
108
+ // in this bucket". Unit-based to match the historical bucket-header
109
+ // semantics ("8 succeeded" = 8 units, not 4 docs × 2 locales).
110
+ const matchingUnitCount = getBucketCountForFilter(bucket, activeStatusFilter);
111
+ // Filter-aware DOC count — independent from unit count above.
112
+ // Drives the auto-load gate + the "render empty bucket" path:
113
+ // we need at least one matching DOC for there to be any rows
114
+ // to fetch / render. A bucket with 4 succeeded units but only 4
115
+ // failed locales spread across the same 4 docs renders 4 rows,
116
+ // not 8.
117
+ const matchingDocCount = activeStatusFilter === 'all' ? bucket.totalDocs : bucket.docCountsByStatus[activeStatusFilter] ?? 0;
118
+ // The needs-load gate compares "have we loaded any rows in this
119
+ // bucket?" to "are there any docs in this bucket at all under the
120
+ // active filter?". When the filter excludes this bucket entirely
121
+ // (matchingDocCount === 0), we never auto-load — the parent filters
122
+ // the bucket out via `shouldBucketBeVisibleUnderFilter`, but the
123
+ // bucket can still flash through this code path during the filter
124
+ // transition tick.
125
+ const needsLoad = bucket.loadedDocs === 0 && matchingDocCount > 0;
126
+ // v1.2.7: filter the displayed docs by the active filter so a doc
127
+ // that only contains succeeded locales doesn't render under
128
+ // `Failed`. Buckets with mixed-status docs (one failed + one
129
+ // succeeded) still show the doc under both filters — that matches
130
+ // the bucket-count semantics (the doc lights up in both buckets).
131
+ const visibleDocs = [
132
+ ...bucket.docs.values()
133
+ ].filter((doc)=>{
134
+ if (activeStatusFilter === 'all') return true;
135
+ const target = activeStatusFilter === 'completed' ? 'success' : activeStatusFilter;
136
+ return doc.jobs.some((j)=>j.status === target);
137
+ });
138
+ // Notify parent on expand/collapse so it can track which buckets are
139
+ // open across filter changes. Idempotent on remount via the initial
140
+ // call below.
141
+ useEffect(()=>{
142
+ onExpandedChange?.(bucket.collection, expanded);
143
+ }, [
144
+ expanded,
145
+ bucket.collection,
146
+ onExpandedChange
147
+ ]);
148
+ // Auto-load this bucket's docs the first time it's expanded if the
149
+ // initial paginated batch page didn't include any units from this
150
+ // collection (the common case for non-`posts` buckets — pagination
151
+ // starts at the front of the unit list which is usually one
152
+ // collection's slice).
153
+ useEffect(()=>{
154
+ if (!expanded || !needsLoad || loading) return;
155
+ let cancelled = false;
156
+ void (async ()=>{
157
+ setLoading(true);
158
+ setLoadError(null);
159
+ try {
160
+ await onLoadBucketDocs(bucket.collection);
161
+ } catch (e) {
162
+ if (!cancelled) {
163
+ setLoadError(e instanceof Error ? e.message : String(e));
164
+ }
165
+ } finally{
166
+ if (!cancelled) setLoading(false);
167
+ }
168
+ })();
169
+ return ()=>{
170
+ cancelled = true;
171
+ };
172
+ // We deliberately depend only on (expanded, needsLoad, bucket.collection).
173
+ // `loading` is the in-flight guard handled inside the effect.
174
+ // eslint-disable-next-line react-hooks/exhaustive-deps
175
+ }, [
176
+ expanded,
177
+ needsLoad,
178
+ bucket.collection
179
+ ]);
180
+ return /*#__PURE__*/ _jsxs(_Fragment, {
181
+ children: [
182
+ /*#__PURE__*/ _jsx("tr", {
183
+ children: /*#__PURE__*/ _jsxs("td", {
184
+ colSpan: 7,
185
+ style: {
186
+ padding: 0
187
+ },
188
+ children: [
189
+ /*#__PURE__*/ _jsxs("div", {
190
+ "aria-expanded": expanded,
191
+ onClick: ()=>setExpanded((v)=>!v),
192
+ onKeyDown: (e)=>{
193
+ if (e.key === 'Enter' || e.key === ' ') {
194
+ e.preventDefault();
195
+ setExpanded((v)=>!v);
196
+ }
197
+ },
198
+ role: "button",
199
+ style: BUCKET_HEADER_STYLE,
200
+ tabIndex: 0,
201
+ children: [
202
+ /*#__PURE__*/ _jsx("span", {
203
+ style: {
204
+ ...CHEVRON_STYLE,
205
+ transform: expanded ? 'rotate(90deg)' : 'rotate(0deg)'
206
+ },
207
+ children: "▶"
208
+ }),
209
+ /*#__PURE__*/ _jsx("span", {
210
+ style: {
211
+ fontWeight: 600
212
+ },
213
+ children: bucket.collection
214
+ }),
215
+ bucket.isGlobal && /*#__PURE__*/ _jsx("span", {
216
+ style: GLOBAL_BADGE_STYLE,
217
+ children: "global"
218
+ }),
219
+ /*#__PURE__*/ _jsxs("span", {
220
+ style: {
221
+ marginLeft: 'auto',
222
+ display: 'flex',
223
+ alignItems: 'center',
224
+ gap: '0.75rem'
225
+ },
226
+ children: [
227
+ activeStatusFilter !== 'all' ? /*#__PURE__*/ _jsxs("span", {
228
+ style: {
229
+ ...PILL_STYLE_BASE,
230
+ color: activeStatusFilter === 'failed' ? 'var(--theme-error-500, #b91c1c)' : activeStatusFilter === 'running' || activeStatusFilter === 'pending' ? 'var(--theme-warning-500, #d97706)' : activeStatusFilter === 'completed' ? 'var(--theme-success-500, #16a34a)' : 'var(--theme-elevation-600)'
231
+ },
232
+ title: `Units in this ${bucket.isGlobal ? 'global' : 'collection'} with status ${activeStatusFilter === 'completed' ? 'succeeded' : activeStatusFilter}.`,
233
+ children: [
234
+ /*#__PURE__*/ _jsx("span", {
235
+ style: {
236
+ fontSize: '0.5rem'
237
+ },
238
+ children: "●"
239
+ }),
240
+ matchingUnitCount,
241
+ ' ',
242
+ activeStatusFilter === 'completed' ? 'succeeded' : activeStatusFilter
243
+ ]
244
+ }) : /*#__PURE__*/ _jsxs(_Fragment, {
245
+ children: [
246
+ bucket.succeededCount > 0 && /*#__PURE__*/ _jsxs("span", {
247
+ style: {
248
+ ...PILL_STYLE_BASE,
249
+ color: hasFailures ? 'var(--theme-elevation-600)' // muted when failure pill is also on screen
250
+ : 'var(--theme-success-500, #16a34a)'
251
+ },
252
+ children: [
253
+ /*#__PURE__*/ _jsx("span", {
254
+ style: {
255
+ fontSize: '0.5rem'
256
+ },
257
+ children: "●"
258
+ }),
259
+ bucket.succeededCount,
260
+ " succeeded"
261
+ ]
262
+ }),
263
+ bucket.failedCount > 0 && /*#__PURE__*/ _jsxs("span", {
264
+ style: {
265
+ ...PILL_STYLE_BASE,
266
+ color: 'var(--theme-error-500, #b91c1c)'
267
+ },
268
+ children: [
269
+ /*#__PURE__*/ _jsx("span", {
270
+ style: {
271
+ fontSize: '0.5rem'
272
+ },
273
+ children: "●"
274
+ }),
275
+ bucket.failedCount,
276
+ " failed"
277
+ ]
278
+ }),
279
+ hasInflight && /*#__PURE__*/ _jsxs("span", {
280
+ style: {
281
+ ...PILL_STYLE_BASE,
282
+ color: 'var(--theme-warning-500, #d97706)'
283
+ },
284
+ children: [
285
+ /*#__PURE__*/ _jsx("span", {
286
+ style: {
287
+ fontSize: '0.5rem'
288
+ },
289
+ children: "●"
290
+ }),
291
+ bucket.pendingCount + bucket.runningCount,
292
+ " in flight"
293
+ ]
294
+ }),
295
+ bucket.skippedCount > 0 && /*#__PURE__*/ _jsxs("span", {
296
+ style: {
297
+ ...PILL_STYLE_BASE,
298
+ color: 'var(--theme-elevation-600)'
299
+ },
300
+ children: [
301
+ bucket.skippedCount,
302
+ " skipped"
303
+ ]
304
+ })
305
+ ]
306
+ }),
307
+ bucket.collectionSpanMs !== null && /*#__PURE__*/ _jsxs("span", {
308
+ style: {
309
+ ...PILL_STYLE_BASE,
310
+ color: 'var(--theme-elevation-600)',
311
+ fontFamily: 'monospace'
312
+ },
313
+ title: bucket.aiActiveMs !== null || bucket.queueWaitMs !== null ? `Collection span — wall-clock from this bucket's first unit start to last completion.\nAI active: ${fmtBucketAiActive(bucket)} · Queued: ${fmtBucketQueuedHelper(bucket)}\n"Queued" is the gap between worker pickup and AI execution (rate-limit / throttle wait). It's a system-level wait, not per-bucket work — buckets processed later in the same batch inherit the queue depth from earlier buckets.` : 'Wall-clock from first unit start to last completion within this collection.',
314
+ children: [
315
+ formatDuration(bucket.collectionSpanMs),
316
+ (bucket.aiActiveMs !== null || bucket.queueWaitMs !== null) && /*#__PURE__*/ _jsxs("span", {
317
+ style: {
318
+ marginLeft: '0.35rem',
319
+ fontSize: '0.625rem',
320
+ color: 'var(--theme-elevation-500)'
321
+ },
322
+ children: [
323
+ "(AI ",
324
+ fmtBucketAiActive(bucket),
325
+ " · queued ",
326
+ fmtBucketQueuedHelper(bucket),
327
+ ")"
328
+ ]
329
+ })
330
+ ]
331
+ }),
332
+ hasFailures && /*#__PURE__*/ _jsx("button", {
333
+ disabled: isRetrying || hasInflight,
334
+ onClick: (e)=>{
335
+ e.stopPropagation();
336
+ onRetryBucket(bucket.collection);
337
+ },
338
+ style: {
339
+ ...RETRY_BUTTON_STYLE,
340
+ opacity: isRetrying || hasInflight ? 0.5 : 1,
341
+ cursor: isRetrying || hasInflight ? 'not-allowed' : 'pointer'
342
+ },
343
+ title: hasInflight ? 'Wait for in-flight units to finish before retrying.' : `Retry the ${bucket.failedCount} failed unit${bucket.failedCount === 1 ? '' : 's'} in this ${bucket.isGlobal ? 'global' : 'collection'}.`,
344
+ type: "button",
345
+ children: isRetrying ? 'Retrying…' : `Retry ${bucket.failedCount} failed`
346
+ })
347
+ ]
348
+ })
349
+ ]
350
+ }),
351
+ expanded && bucket.distinctFailureCodes > 1 && // v1.2.7: suppress the "N different error types" summary
352
+ // when the active filter is non-failure — the editor is
353
+ // explicitly looking at succeeded / pending / etc., so the
354
+ // failure breakdown is noise.
355
+ (activeStatusFilter === 'all' || activeStatusFilter === 'failed') && (()=>{
356
+ // ROUND3-8: route the bucket-summary error-types through
357
+ // the same `categorizeFailure` helper the per-unit
358
+ // drill-down uses (NEW-12 / Group H). Previously the
359
+ // summary keyed off the raw persisted `failureCode` and
360
+ // rendered "Translation system is not set up" for
361
+ // cancel-mid-flight units while the drill-down correctly
362
+ // said "Stopped mid-run". Editors who didn't drill in saw
363
+ // a misleading config-broken signal. Threading
364
+ // `batchStatus` through the categorizer collapses both
365
+ // surfaces onto the same story. Pure helper extracted to
366
+ // `bucketFailureSummary.ts` so the grouping logic is
367
+ // unit-tested.
368
+ const summaryEntries = summarizeBucketFailureTypes(bucket.topFailureCodes, batchStatus);
369
+ // If everything collapsed to one bucket post-categorisation
370
+ // the "different error types" framing no longer applies —
371
+ // suppress the line entirely, the unit-level drill-down is
372
+ // the authoritative surface.
373
+ if (summaryEntries.length <= 1) return null;
374
+ return /*#__PURE__*/ _jsxs("div", {
375
+ style: {
376
+ padding: '0.375rem 1rem 0.375rem 2.25rem',
377
+ fontSize: '0.75rem',
378
+ color: 'var(--theme-warning-500, #d97706)',
379
+ background: 'var(--theme-elevation-0)',
380
+ borderBottom: '1px solid var(--theme-elevation-100)'
381
+ },
382
+ children: [
383
+ summaryEntries.length,
384
+ " different error type",
385
+ summaryEntries.length === 1 ? '' : 's',
386
+ " in this",
387
+ ' ',
388
+ bucket.isGlobal ? 'global' : 'collection',
389
+ " —",
390
+ ' ',
391
+ summaryEntries.map(({ title, count })=>`${title} (${count})`).join(', '),
392
+ summaryEntries.length > 2 && '. See rows for full breakdown.'
393
+ ]
394
+ });
395
+ })()
396
+ ]
397
+ })
398
+ }),
399
+ expanded && loading && /*#__PURE__*/ _jsx("tr", {
400
+ children: /*#__PURE__*/ _jsxs("td", {
401
+ colSpan: 7,
402
+ style: {
403
+ padding: '0.5rem 1rem 0.5rem 2.25rem',
404
+ fontSize: '0.75rem',
405
+ color: 'var(--theme-elevation-600)',
406
+ background: 'var(--theme-elevation-0)',
407
+ borderBottom: '1px solid var(--theme-elevation-100)'
408
+ },
409
+ children: [
410
+ "Loading ",
411
+ bucket.totalDocs,
412
+ " doc",
413
+ bucket.totalDocs === 1 ? '' : 's',
414
+ " in this",
415
+ ' ',
416
+ bucket.isGlobal ? 'global' : 'collection',
417
+ "…"
418
+ ]
419
+ })
420
+ }),
421
+ expanded && !loading && loadError && /*#__PURE__*/ _jsx("tr", {
422
+ children: /*#__PURE__*/ _jsxs("td", {
423
+ colSpan: 7,
424
+ style: {
425
+ padding: '0.5rem 1rem 0.5rem 2.25rem',
426
+ fontSize: '0.75rem',
427
+ color: 'var(--theme-error-500, #b91c1c)',
428
+ background: 'var(--theme-elevation-0)',
429
+ borderBottom: '1px solid var(--theme-elevation-100)'
430
+ },
431
+ children: [
432
+ "Could not load the docs for this ",
433
+ bucket.isGlobal ? 'global' : 'collection',
434
+ ". Refresh and try again."
435
+ ]
436
+ })
437
+ }),
438
+ expanded && visibleDocs.map((doc)=>/*#__PURE__*/ _jsx(DocRow, {
439
+ basePath: basePath,
440
+ batchStatus: batchStatus,
441
+ doc: doc,
442
+ isGlobal: bucket.isGlobal,
443
+ isRetrying: retryingDocKey === `${doc.collection}/${doc.documentId}`,
444
+ onRetry: ()=>onRetryDoc(doc.collection, doc.documentId)
445
+ }, `${doc.collection}-${doc.documentId}`)),
446
+ expanded && !loading && !loadError && visibleDocs.length === 0 && matchingDocCount > 0 && /*#__PURE__*/ _jsx("tr", {
447
+ children: /*#__PURE__*/ _jsxs("td", {
448
+ colSpan: 7,
449
+ style: {
450
+ padding: '0.5rem 1rem 0.5rem 2.25rem',
451
+ fontSize: '0.75rem',
452
+ color: 'var(--theme-elevation-500)',
453
+ background: 'var(--theme-elevation-0)',
454
+ borderBottom: '1px solid var(--theme-elevation-100)'
455
+ },
456
+ children: [
457
+ "Loading ",
458
+ matchingDocCount,
459
+ " matching doc",
460
+ matchingDocCount === 1 ? '' : 's',
461
+ " for the active filter…"
462
+ ]
463
+ })
464
+ }),
465
+ expanded && !loading && !loadError && bucket.loadedDocs > 0 && hasMoreInBucket && /*#__PURE__*/ _jsx("tr", {
466
+ children: /*#__PURE__*/ _jsx("td", {
467
+ colSpan: 7,
468
+ style: {
469
+ padding: '0.5rem 1rem 0.6rem 2.25rem',
470
+ background: 'var(--theme-elevation-0)',
471
+ borderBottom: '1px solid var(--theme-elevation-100)'
472
+ },
473
+ children: /*#__PURE__*/ _jsx("button", {
474
+ type: "button",
475
+ disabled: isLoadingMoreInBucket,
476
+ onClick: ()=>onLoadBucketDocs(bucket.collection),
477
+ style: {
478
+ padding: '0.3rem 0.75rem',
479
+ background: 'transparent',
480
+ border: '1px solid var(--theme-elevation-200)',
481
+ borderRadius: '4px',
482
+ color: 'var(--theme-elevation-700)',
483
+ fontSize: '0.75rem',
484
+ cursor: isLoadingMoreInBucket ? 'not-allowed' : 'pointer'
485
+ },
486
+ children: isLoadingMoreInBucket ? 'Loading…' : `Load more in ${bucket.collection} (${bucket.loadedDocs} of ${bucket.totalDocs} loaded)`
487
+ })
488
+ })
489
+ })
490
+ ]
491
+ });
492
+ };
493
+ // ---------------------------------------------------------------------------
494
+ // DocRow — one row per (collection, doc) with locale chips inline
495
+ // ---------------------------------------------------------------------------
496
+ const LOCALE_CHIP_STYLE = {
497
+ display: 'inline-flex',
498
+ alignItems: 'center',
499
+ gap: '0.25rem',
500
+ fontSize: '0.6875rem',
501
+ fontWeight: 500,
502
+ padding: '0.125rem 0.4rem',
503
+ borderRadius: '3px',
504
+ cursor: 'default'
505
+ };
506
+ const STATUS_LABEL = {
507
+ success: 'Succeeded',
508
+ failed: 'Failed',
509
+ pending: 'Pending',
510
+ running: 'Running',
511
+ skipped: 'Skipped',
512
+ reverted: 'Reverted'
513
+ };
514
+ const STATUS_COLOR = {
515
+ success: 'var(--theme-success-500, #16a34a)',
516
+ failed: 'var(--theme-error-500, #b91c1c)',
517
+ pending: 'var(--theme-elevation-500)',
518
+ running: 'var(--theme-warning-500, #d97706)',
519
+ skipped: 'var(--theme-elevation-500)',
520
+ reverted: 'var(--theme-elevation-500)'
521
+ };
522
+ const DocRow = ({ basePath, batchStatus, doc, isGlobal, isRetrying, onRetry })=>{
523
+ // Auto-expand any doc that has failures so the editor sees the
524
+ // failure detail without clicking — the UX call from the audit:
525
+ // "the problem hits the editor before any click."
526
+ const [expanded, setExpanded] = useState(doc.failedLocaleCount > 0);
527
+ const hasFailures = doc.failedLocaleCount > 0;
528
+ return /*#__PURE__*/ _jsx(_Fragment, {
529
+ children: /*#__PURE__*/ _jsx("tr", {
530
+ children: /*#__PURE__*/ _jsxs("td", {
531
+ colSpan: 7,
532
+ style: {
533
+ padding: '0.4rem 1rem 0.4rem 2.25rem',
534
+ borderBottom: '1px solid var(--theme-elevation-100)',
535
+ background: hasFailures ? 'var(--theme-error-50, rgba(185,28,28,0.04))' : 'var(--theme-elevation-0)',
536
+ borderLeft: hasFailures ? '3px solid var(--theme-error-500, #b91c1c)' : '3px solid transparent',
537
+ fontSize: '0.8125rem'
538
+ },
539
+ children: [
540
+ /*#__PURE__*/ _jsxs("div", {
541
+ "aria-expanded": expanded,
542
+ onClick: ()=>setExpanded((v)=>!v),
543
+ onKeyDown: (e)=>{
544
+ if (e.key === 'Enter' || e.key === ' ') {
545
+ e.preventDefault();
546
+ setExpanded((v)=>!v);
547
+ }
548
+ },
549
+ role: "button",
550
+ style: {
551
+ display: 'flex',
552
+ alignItems: 'center',
553
+ gap: '0.75rem',
554
+ cursor: 'pointer',
555
+ userSelect: 'none'
556
+ },
557
+ tabIndex: 0,
558
+ children: [
559
+ /*#__PURE__*/ _jsx("span", {
560
+ style: {
561
+ color: 'var(--theme-elevation-500)',
562
+ fontSize: '0.7rem',
563
+ width: '0.7rem',
564
+ display: 'inline-block',
565
+ transition: 'transform 120ms ease',
566
+ transform: expanded ? 'rotate(90deg)' : 'rotate(0deg)'
567
+ },
568
+ children: "▶"
569
+ }),
570
+ /*#__PURE__*/ _jsx("span", {
571
+ style: {
572
+ minWidth: '5rem',
573
+ color: 'var(--theme-elevation-1000)'
574
+ },
575
+ children: !isOpenableDocId(doc.documentId) ? /*#__PURE__*/ _jsx("span", {
576
+ style: {
577
+ color: 'var(--theme-elevation-500)',
578
+ fontStyle: 'italic',
579
+ fontFamily: 'inherit'
580
+ },
581
+ title: "The unit's document id wasn't resolved before the batch was cancelled — no document to open.",
582
+ children: "(id unresolved)"
583
+ }) : /*#__PURE__*/ _jsx("a", {
584
+ // 2026-06-05: globals route through globalHref; previous
585
+ // code always used docHref and built
586
+ // `/admin/collections/<slug>/<slug>` for globals like
587
+ // `translation-keys` — a 404. The deeper per-locale
588
+ // panel below (~line 909) already had this fix; this
589
+ // is the per-doc row link, same fix needed here.
590
+ href: (isGlobal ? globalHref(basePath, doc.collection, null) : docHref(basePath, doc.collection, doc.documentId, null)) ?? '#',
591
+ onClick: (e)=>e.stopPropagation(),
592
+ rel: "noopener noreferrer",
593
+ style: {
594
+ color: 'var(--theme-text-link, #2563eb)',
595
+ textDecoration: 'none',
596
+ fontFamily: 'monospace'
597
+ },
598
+ target: "_blank",
599
+ children: isGlobal ? doc.collection : `#${doc.documentId}`
600
+ })
601
+ }),
602
+ /*#__PURE__*/ _jsx("div", {
603
+ style: {
604
+ display: 'flex',
605
+ flexWrap: 'wrap',
606
+ gap: '0.4rem'
607
+ },
608
+ children: doc.jobs.map((job)=>/*#__PURE__*/ _jsx(LocaleChip, {
609
+ job: job
610
+ }, job.locale))
611
+ }),
612
+ /*#__PURE__*/ _jsxs("span", {
613
+ style: {
614
+ marginLeft: 'auto',
615
+ display: 'flex',
616
+ alignItems: 'center',
617
+ gap: '1rem',
618
+ fontSize: '0.75rem',
619
+ color: 'var(--theme-elevation-600)',
620
+ fontFamily: 'monospace'
621
+ },
622
+ children: [
623
+ /*#__PURE__*/ _jsxs("span", {
624
+ title: doc.docAiActiveMs !== null && doc.docQueueWaitMs !== null ? `Doc span — from first locale start to last completion.\nAI active: ${formatDuration(doc.docAiActiveMs)} · Queue wait: ${formatDuration(doc.docQueueWaitMs)}${doc.maxAttempts > 1 ? '\nIncludes retry attempts.' : ''}` : "Doc span — from this doc's first locale start to its last completion. Includes any throttle queue wait. Doc-level AI compute time wasn't recorded on this batch (legacy data).",
625
+ children: [
626
+ formatDuration(doc.docSpanMs),
627
+ doc.docAiActiveMs !== null && doc.docQueueWaitMs !== null && /*#__PURE__*/ _jsxs("span", {
628
+ style: {
629
+ marginLeft: '0.35rem',
630
+ fontSize: '0.625rem',
631
+ color: 'var(--theme-elevation-500)'
632
+ },
633
+ children: [
634
+ "(AI ",
635
+ formatDuration(doc.docAiActiveMs),
636
+ ")"
637
+ ]
638
+ }),
639
+ doc.maxAttempts > 1 && /*#__PURE__*/ _jsx("span", {
640
+ style: {
641
+ marginLeft: '0.35rem',
642
+ fontSize: '0.625rem',
643
+ color: 'var(--theme-warning-500, #d97706)'
644
+ },
645
+ children: "(incl. retries)"
646
+ })
647
+ ]
648
+ }),
649
+ /*#__PURE__*/ _jsx("span", {
650
+ title: "Total cost across all locales for this doc",
651
+ children: formatCost(doc.totalCostUsd)
652
+ }),
653
+ doc.maxAttempts > 1 && /*#__PURE__*/ _jsxs("span", {
654
+ style: {
655
+ color: 'var(--theme-warning-500, #d97706)'
656
+ },
657
+ title: "Highest attempt count across this doc's locales",
658
+ children: [
659
+ doc.maxAttempts,
660
+ "× attempts"
661
+ ]
662
+ }),
663
+ hasFailures && /*#__PURE__*/ _jsx("button", {
664
+ disabled: isRetrying,
665
+ onClick: (e)=>{
666
+ e.stopPropagation();
667
+ onRetry();
668
+ },
669
+ style: {
670
+ border: '1px solid var(--theme-error-500, #b91c1c)',
671
+ color: 'var(--theme-error-500, #b91c1c)',
672
+ background: 'transparent',
673
+ borderRadius: '4px',
674
+ padding: '0.1rem 0.5rem',
675
+ fontSize: '0.6875rem',
676
+ fontWeight: 500,
677
+ cursor: isRetrying ? 'wait' : 'pointer',
678
+ opacity: isRetrying ? 0.5 : 1
679
+ },
680
+ title: `Retry the ${doc.failedLocaleCount} failed locale${doc.failedLocaleCount === 1 ? '' : 's'} for this doc.`,
681
+ type: "button",
682
+ children: isRetrying ? 'Retrying…' : `Retry ${doc.failedLocaleCount}`
683
+ })
684
+ ]
685
+ })
686
+ ]
687
+ }),
688
+ expanded && /*#__PURE__*/ _jsx("div", {
689
+ style: {
690
+ marginTop: '0.5rem',
691
+ display: 'flex',
692
+ flexDirection: 'column',
693
+ gap: '0.4rem'
694
+ },
695
+ children: doc.jobs.map((job)=>/*#__PURE__*/ _jsx(LocaleDetailRow, {
696
+ basePath: basePath,
697
+ batchStatus: batchStatus,
698
+ collection: doc.collection,
699
+ documentId: doc.documentId,
700
+ isGlobal: isGlobal,
701
+ job: job
702
+ }, `detail-${job.locale}`))
703
+ })
704
+ ]
705
+ })
706
+ })
707
+ });
708
+ };
709
+ const LocaleDetailRow = ({ basePath, batchStatus, collection, documentId, isGlobal, job })=>{
710
+ const isFailed = job.status === 'failed';
711
+ // NEW-12 (v1.2.6): route through the categorizer so cancel-mid-flight
712
+ // units stop showing "Translation system is not set up" copy when 90%
713
+ // of the batch succeeded. The categorizer also corrects pre-v1.2.6
714
+ // persisted rows where `failureCode` says `permanent.config` but the
715
+ // raw message clearly says "Provider returned undefined / non-finite".
716
+ const categorized = categorizeFailure({
717
+ failureCode: job.failureCode,
718
+ failureMessage: job.failureMessage,
719
+ batchStatus,
720
+ attemptCount: job.attempts
721
+ });
722
+ const code = categorized.editorCode;
723
+ // ROUND5-2 (v1.2.6): suppress the `Open →` link when the doc id
724
+ // wasn't resolved (cancel-mid-flight). ROUND3-7 patched the row
725
+ // summary; this is the deeper detail panel. Building docHref with
726
+ // a null id produced `/admin/collections/posts/null?locale=es`
727
+ // which lands the editor on Payload's "document not found" page.
728
+ // v1.2.7: globals are addressed by slug only (`/admin/globals/<slug>`),
729
+ // never by id. Pre-v1.2.7 the link always used `docHref` which built
730
+ // `/admin/collections/translation-keys/translation-keys?locale=es` —
731
+ // a 404 because translation-keys is a global. Route through the right
732
+ // builder per surface kind.
733
+ const localeHref = isOpenableDocId(documentId) ? isGlobal ? globalHref(basePath, collection, job.locale) : docHref(basePath, collection, documentId, job.locale) : null;
734
+ return /*#__PURE__*/ _jsxs("div", {
735
+ style: {
736
+ padding: '0.5rem 0.75rem',
737
+ background: 'var(--theme-elevation-50, #f8fafc)',
738
+ border: '1px solid var(--theme-elevation-100)',
739
+ borderLeft: isFailed ? '3px solid var(--theme-error-500, #b91c1c)' : `3px solid ${STATUS_COLOR[job.status] ?? 'var(--theme-elevation-200)'}`,
740
+ borderRadius: '4px',
741
+ fontSize: '0.75rem'
742
+ },
743
+ children: [
744
+ /*#__PURE__*/ _jsxs("div", {
745
+ style: {
746
+ display: 'grid',
747
+ gridTemplateColumns: 'minmax(2.5rem, auto) minmax(5rem, auto) repeat(3, minmax(0, 1fr)) auto',
748
+ gap: '1rem',
749
+ alignItems: 'center'
750
+ },
751
+ children: [
752
+ /*#__PURE__*/ _jsx("span", {
753
+ style: {
754
+ fontFamily: 'monospace',
755
+ fontWeight: 600,
756
+ color: 'var(--theme-elevation-1000)'
757
+ },
758
+ children: job.locale
759
+ }),
760
+ /*#__PURE__*/ _jsxs("span", {
761
+ style: {
762
+ color: STATUS_COLOR[job.status] ?? 'var(--theme-elevation-700)',
763
+ fontWeight: 500,
764
+ display: 'inline-flex',
765
+ alignItems: 'center',
766
+ gap: '0.3rem'
767
+ },
768
+ children: [
769
+ /*#__PURE__*/ _jsx("span", {
770
+ style: {
771
+ fontSize: '0.5rem'
772
+ },
773
+ children: "●"
774
+ }),
775
+ STATUS_LABEL[job.status] ?? job.status
776
+ ]
777
+ }),
778
+ /*#__PURE__*/ _jsx(DataField, {
779
+ label: "AI time",
780
+ value: formatDuration(job.processingDurationMs),
781
+ hint: "Provider call latency. Excludes throttle queue wait — that lives at the collection level."
782
+ }),
783
+ /*#__PURE__*/ _jsx(DataField, {
784
+ label: "Cost",
785
+ value: formatCost(job.costUsd),
786
+ hint: "USD billed for this locale's translation"
787
+ }),
788
+ /*#__PURE__*/ _jsx(DataField, {
789
+ label: "Attempts",
790
+ value: String(job.attempts),
791
+ hint: "Worker attempts (>1 means retried). Matches the count in the persistent-failure alert.",
792
+ tone: job.attempts > 1 ? 'warning' : 'default'
793
+ }),
794
+ localeHref && /*#__PURE__*/ _jsx("a", {
795
+ href: localeHref,
796
+ onClick: (e)=>e.stopPropagation(),
797
+ rel: "noopener noreferrer",
798
+ style: {
799
+ color: 'var(--theme-text-link, #2563eb)',
800
+ textDecoration: 'none',
801
+ fontSize: '0.7rem',
802
+ fontWeight: 500
803
+ },
804
+ target: "_blank",
805
+ title: `Open this document in ${job.locale}`,
806
+ children: "Open →"
807
+ })
808
+ ]
809
+ }),
810
+ (job.startedAt || job.completedAt) && /*#__PURE__*/ _jsxs("div", {
811
+ style: {
812
+ marginTop: '0.35rem',
813
+ fontSize: '0.65rem',
814
+ color: 'var(--theme-elevation-500)',
815
+ fontFamily: 'monospace',
816
+ display: 'flex',
817
+ gap: '1rem'
818
+ },
819
+ children: [
820
+ job.startedAt && /*#__PURE__*/ _jsxs("span", {
821
+ children: [
822
+ "Started: ",
823
+ fmtTimestamp(job.startedAt)
824
+ ]
825
+ }),
826
+ job.completedAt && /*#__PURE__*/ _jsxs("span", {
827
+ children: [
828
+ "Completed: ",
829
+ fmtTimestamp(job.completedAt)
830
+ ]
831
+ })
832
+ ]
833
+ }),
834
+ isFailed && /*#__PURE__*/ _jsx("div", {
835
+ style: {
836
+ marginTop: '0.5rem'
837
+ },
838
+ children: /*#__PURE__*/ _jsx(EditorError, {
839
+ code: code,
840
+ compact: true,
841
+ context: {
842
+ locale: job.locale
843
+ },
844
+ details: categorized.details
845
+ })
846
+ })
847
+ ]
848
+ });
849
+ };
850
+ // One column of label + value in the LocaleDetailRow grid.
851
+ const DataField = ({ label, value, hint, tone = 'default' })=>/*#__PURE__*/ _jsxs("span", {
852
+ style: {
853
+ display: 'flex',
854
+ flexDirection: 'column',
855
+ gap: '0.1rem',
856
+ fontSize: '0.7rem'
857
+ },
858
+ title: hint,
859
+ children: [
860
+ /*#__PURE__*/ _jsx("span", {
861
+ style: {
862
+ color: 'var(--theme-elevation-500)',
863
+ textTransform: 'uppercase',
864
+ letterSpacing: '0.03em',
865
+ fontSize: '0.6rem',
866
+ fontWeight: 600
867
+ },
868
+ children: label
869
+ }),
870
+ /*#__PURE__*/ _jsx("span", {
871
+ style: {
872
+ color: tone === 'warning' ? 'var(--theme-warning-500, #d97706)' : 'var(--theme-elevation-900)',
873
+ fontFamily: 'monospace'
874
+ },
875
+ children: value
876
+ })
877
+ ]
878
+ });
879
+ // ---------------------------------------------------------------------------
880
+ // LocaleChip — colored pill per locale, rich tooltip
881
+ // ---------------------------------------------------------------------------
882
+ const STATUS_TO_CHIP = {
883
+ success: {
884
+ background: 'transparent',
885
+ color: 'var(--theme-elevation-700)',
886
+ dot: 'var(--theme-success-500, #16a34a)',
887
+ glyph: '●'
888
+ },
889
+ failed: {
890
+ background: 'var(--theme-error-100, #fee2e2)',
891
+ color: 'var(--theme-error-500, #b91c1c)',
892
+ dot: 'var(--theme-error-500, #b91c1c)',
893
+ glyph: '●'
894
+ },
895
+ // v1.2.5: outlined dot + faded text so pending is unambiguously
896
+ // "queued, hasn't started." Pre-1.2.5 pending rendered identical
897
+ // to success (same transparent bg, same filled glyph, same elev-700
898
+ // text) — editors couldn't tell finished work from queued work.
899
+ pending: {
900
+ background: 'transparent',
901
+ color: 'var(--theme-elevation-500)',
902
+ dot: 'var(--theme-elevation-500)',
903
+ glyph: '○'
904
+ },
905
+ // v1.2.5: animate the dot so an editor can see "this is actively
906
+ // running right now" without needing the chip to update.
907
+ running: {
908
+ background: 'var(--theme-warning-100, #fef3c7)',
909
+ color: 'var(--theme-warning-500, #d97706)',
910
+ dot: 'var(--theme-warning-500, #d97706)',
911
+ glyph: '●',
912
+ animate: true
913
+ },
914
+ skipped: {
915
+ background: 'transparent',
916
+ color: 'var(--theme-elevation-600)',
917
+ dot: 'var(--theme-elevation-500)',
918
+ glyph: '●'
919
+ }
920
+ };
921
+ const LocaleChip = ({ job })=>{
922
+ const chip = STATUS_TO_CHIP[job.status] ?? STATUS_TO_CHIP.skipped;
923
+ // Rich tooltip — quick stats without expanding the row. Falls back to
924
+ // a short summary when we don't have processing/cost data on legacy
925
+ // rows.
926
+ const parts = [
927
+ `${job.locale} — ${STATUS_LABEL[job.status] ?? job.status}`
928
+ ];
929
+ if (typeof job.processingDurationMs === 'number' && job.processingDurationMs > 0) {
930
+ parts.push(formatDuration(job.processingDurationMs));
931
+ }
932
+ if (typeof job.costUsd === 'number' && job.costUsd > 0) {
933
+ parts.push(formatCost(job.costUsd));
934
+ }
935
+ if (job.attempts > 1) {
936
+ parts.push(`${job.attempts} attempts`);
937
+ }
938
+ if (job.failureCode) {
939
+ parts.push(`code: ${job.failureCode}`);
940
+ }
941
+ return /*#__PURE__*/ _jsxs("span", {
942
+ style: {
943
+ ...LOCALE_CHIP_STYLE,
944
+ background: chip.background,
945
+ color: chip.color,
946
+ cursor: 'default',
947
+ border: '1px solid transparent',
948
+ fontFamily: 'monospace'
949
+ },
950
+ title: parts.join(' · '),
951
+ children: [
952
+ /*#__PURE__*/ _jsx("span", {
953
+ style: {
954
+ fontSize: '0.5rem',
955
+ color: chip.dot,
956
+ ...chip.animate ? {
957
+ animation: 'translation-hub-pulse 1.2s ease-in-out infinite'
958
+ } : null
959
+ },
960
+ children: chip.glyph
961
+ }),
962
+ job.locale
963
+ ]
964
+ });
965
+ };
966
+ // v1.2.5: lightweight keyframe inject. Defining `@keyframes` inline
967
+ // via a styled wrapper is the cheapest cross-theme path — Payload's
968
+ // admin shell doesn't expose a global stylesheet hook for plugins.
969
+ // Mounted once at the bottom of LocaleChip so any consumer that
970
+ // renders the chip gets the animation rule installed.
971
+ const PULSE_KEYFRAMES = `
972
+ @keyframes translation-hub-pulse {
973
+ 0%, 100% { opacity: 1; }
974
+ 50% { opacity: 0.35; }
975
+ }
976
+ `;
977
+ if (typeof document !== 'undefined' && !document.getElementById('translation-hub-pulse-keyframes')) {
978
+ const style = document.createElement('style');
979
+ style.id = 'translation-hub-pulse-keyframes';
980
+ style.textContent = PULSE_KEYFRAMES;
981
+ document.head.appendChild(style);
982
+ }