@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,391 @@
1
+ import { DEFAULT_BULK_TRANSLATE_BATCHES_COLLECTION_SLUG } from '../../bulk-translate-batches-collection.js';
2
+ import { DEFAULT_BULK_TRANSLATE_UNITS_COLLECTION_SLUG } from '../../bulk-translate-units-collection.js';
3
+ import { computeLiveBatchCount } from '../../lib/batch-counts.js';
4
+ import { createScopedLogger } from '../../lib/logger.js';
5
+ import { decodeCursor, encodeCursor, errorResponse, isEditorOrAdmin, unauthorizedResponse } from './_helpers.js';
6
+ // ---------------------------------------------------------------------------
7
+ // Handler
8
+ // ---------------------------------------------------------------------------
9
+ /**
10
+ * `GET /api/translation-hub/bulk-translate/:id/status`
11
+ *
12
+ * Returns batch metadata + aggregate counts by unit status + a
13
+ * paginated drill-down list of units (filterable by status).
14
+ *
15
+ * Query params:
16
+ * - `cursor` — opaque cursor returned by the previous page
17
+ * - `limit` — page size (default 20, max 100)
18
+ * - `status` — filter to one unit status (`failed` is the typical
19
+ * value for the failure drill-down UI)
20
+ */ export const getBulkTranslateStatusHandler = (options = {})=>async (req)=>{
21
+ const batchesSlug = options.batchesCollectionSlug ?? DEFAULT_BULK_TRANSLATE_BATCHES_COLLECTION_SLUG;
22
+ const unitsSlug = options.unitsCollectionSlug ?? DEFAULT_BULK_TRANSLATE_UNITS_COLLECTION_SLUG;
23
+ if (!req.user) {
24
+ return unauthorizedResponse(req);
25
+ }
26
+ if (!isEditorOrAdmin(req.user)) {
27
+ return errorResponse('forbidden', "You don't have permission to view bulk-translation status. Contact an admin.", 403);
28
+ }
29
+ const batchId = extractBatchId(req.url ?? '');
30
+ if (!batchId) {
31
+ return errorResponse('invalid_batch_id', 'Batch ID could not be parsed from the request URL.', 400);
32
+ }
33
+ let batch;
34
+ try {
35
+ batch = await req.payload.findByID({
36
+ collection: batchesSlug,
37
+ id: batchId,
38
+ overrideAccess: true,
39
+ depth: 0
40
+ });
41
+ } catch (err) {
42
+ const log = createScopedLogger(req.payload, {
43
+ component: 'hub.status',
44
+ batchId
45
+ });
46
+ log.event('warn', 'hub.status.batch-read.failed', {
47
+ err,
48
+ endpoint: 'hub.status',
49
+ batchId
50
+ });
51
+ batch = undefined;
52
+ }
53
+ if (!batch) {
54
+ return errorResponse('not_found', 'This translation run no longer exists. Refresh the page.', 404);
55
+ }
56
+ const url = new URL(req.url ?? 'http://localhost');
57
+ const cursor = url.searchParams.get('cursor') ?? undefined;
58
+ const limitRaw = url.searchParams.get('limit');
59
+ const statusFilter = url.searchParams.get('status') ?? undefined;
60
+ const collectionFilter = url.searchParams.get('collection') ?? undefined;
61
+ let limit = limitRaw ? Number.parseInt(limitRaw, 10) : 20;
62
+ if (!Number.isFinite(limit) || limit <= 0) limit = 20;
63
+ if (limit > 100) limit = 100;
64
+ const offset = decodeCursor(cursor);
65
+ // Live counts from the units table — bypasses cached counters that
66
+ // drift on retries / cancels / admin SDK interventions. See
67
+ // `lib/batch-counts.ts` for the architecture note.
68
+ const liveCounts = await computeLiveBatchCount(req.payload, unitsSlug, batchId);
69
+ const counts = {
70
+ total: liveCounts.total,
71
+ pending: liveCounts.pending,
72
+ running: liveCounts.running,
73
+ completed: liveCounts.completed,
74
+ failed: liveCounts.failed,
75
+ skipped: liveCounts.skipped,
76
+ reverted: liveCounts.reverted
77
+ };
78
+ const countsByCollection = await aggregateUnitCountsByCollection(req.payload, unitsSlug, batchId);
79
+ const jobs = await fetchUnitPage(req.payload, unitsSlug, batchId, statusFilter, offset, limit, collectionFilter);
80
+ const nextCursor = jobs.length === limit ? encodeCursor(offset + limit) : null;
81
+ return Response.json({
82
+ data: {
83
+ batchId,
84
+ status: batch.status,
85
+ mode: batch.mode,
86
+ canaryLimit: batch.canaryLimit ?? null,
87
+ estimatedCostUsd: batch.estimatedCostUsd ?? null,
88
+ actualCostUsd: batch.actualCostUsd ?? 0,
89
+ counts,
90
+ countsByCollection,
91
+ jobs,
92
+ nextCursor,
93
+ snapshot: {
94
+ providerKey: batch.snapshotProviderKey ?? null,
95
+ modelId: batch.snapshotModelId ?? null,
96
+ sourceLocale: batch.snapshotSourceLocale ?? null
97
+ },
98
+ timestamps: {
99
+ enqueuedAt: batch.enqueuedAt ?? null,
100
+ startedAt: batch.startedAt ?? null,
101
+ completedAt: batch.completedAt ?? null,
102
+ cancelledAt: batch.cancelledAt ?? null,
103
+ revertedAt: batch.revertedAt ?? null
104
+ },
105
+ triggeredByUserId: batch.triggeredByUserId ?? null,
106
+ triggeredByEmail: batch.triggeredByEmail ?? null,
107
+ failures: batch.failures ?? null
108
+ }
109
+ });
110
+ };
111
+ /**
112
+ * Extracts the batch id from a URL of the shape
113
+ * `/api/translation-hub/bulk-translate/<id>/status`. Defensive across
114
+ * URL variants Payload may give us — we look for the segment between
115
+ * `bulk-translate` and `status`.
116
+ */ export function extractBatchId(url) {
117
+ try {
118
+ const segments = new URL(url, 'http://placeholder').pathname.split('/').filter(Boolean);
119
+ const anchorIndex = segments.indexOf('bulk-translate');
120
+ if (anchorIndex === -1) return undefined;
121
+ const candidate = segments[anchorIndex + 1];
122
+ // Reject the literal endpoint paths (no id present).
123
+ if (!candidate || candidate === 'status' || candidate === 'enqueue') {
124
+ return undefined;
125
+ }
126
+ return candidate;
127
+ } catch {
128
+ return undefined;
129
+ }
130
+ }
131
+ /**
132
+ * Per-collection aggregate counts + top-2 failure codes for a batch.
133
+ * Drives the tree DrillDown's bucket-header counts — without this, the
134
+ * UI would have to compute bucket totals from the paginated `jobs` page
135
+ * and the numbers would only reflect "how many of the loaded units are
136
+ * in this bucket", not the actual total.
137
+ *
138
+ * Strategy: one `payload.find` with `limit: 5000`, `depth: 0`, and
139
+ * `select` narrowed to (collection, documentId, status, failureCode).
140
+ * Aggregate in JS — provider-agnostic, no raw SQL. For batches above
141
+ * 5000 units we'd switch to drizzle GROUP BY but realistic batches
142
+ * stay well under that ceiling.
143
+ */ async function aggregateUnitCountsByCollection(payload, unitsSlug, batchId) {
144
+ const out = {};
145
+ let page = 1;
146
+ // eslint-disable-next-line no-constant-condition
147
+ while(true){
148
+ const result = await payload.find({
149
+ collection: unitsSlug,
150
+ where: {
151
+ batchId: {
152
+ equals: batchId
153
+ }
154
+ },
155
+ page,
156
+ limit: 1000,
157
+ depth: 0,
158
+ // Everything the aggregation reads — and nothing else. The
159
+ // unselected `preRunSnapshot` jsonb is the heavy column (tens of
160
+ // KB per unit); without this projection every status-page load
161
+ // dragged the full snapshot payload of all units through
162
+ // Postgres TOAST + JSON.parse.
163
+ select: {
164
+ collection: true,
165
+ documentId: true,
166
+ status: true,
167
+ failureCode: true,
168
+ startedAt: true,
169
+ completedAt: true,
170
+ processingDurationMs: true
171
+ },
172
+ overrideAccess: true
173
+ });
174
+ for (const doc of result.docs){
175
+ const collection = String(doc.collection ?? '');
176
+ const documentId = String(doc.documentId ?? '');
177
+ const status = String(doc.status ?? '');
178
+ const failureCode = typeof doc.failureCode === 'string' ? doc.failureCode : null;
179
+ let bucket = out[collection];
180
+ if (!bucket) {
181
+ bucket = {
182
+ // For globals, the worker writes documentId === collection (the
183
+ // slug doubles as the id).
184
+ isGlobal: documentId === collection,
185
+ total: 0,
186
+ pending: 0,
187
+ running: 0,
188
+ succeeded: 0,
189
+ failed: 0,
190
+ skipped: 0,
191
+ reverted: 0,
192
+ _failureCodes: new Map(),
193
+ _docIds: new Set(),
194
+ _docIdsByPending: new Set(),
195
+ _docIdsByRunning: new Set(),
196
+ _docIdsBySucceeded: new Set(),
197
+ _docIdsByFailed: new Set(),
198
+ _docIdsBySkipped: new Set(),
199
+ _docIdsByReverted: new Set(),
200
+ _minStartedAtMs: null,
201
+ _maxCompletedAtMs: null,
202
+ _aiActiveMs: 0,
203
+ _aiActiveSeen: false
204
+ };
205
+ out[collection] = bucket;
206
+ }
207
+ bucket._docIds.add(documentId);
208
+ bucket.total += 1;
209
+ // Per-collection duration aggregates. min(startedAt) anchors the
210
+ // "when did we start translating this collection" moment;
211
+ // max(completedAt) is the last unit finish.
212
+ const startedAtRaw = doc.startedAt;
213
+ if (typeof startedAtRaw === 'string') {
214
+ const ms = new Date(startedAtRaw).getTime();
215
+ if (Number.isFinite(ms)) {
216
+ if (bucket._minStartedAtMs === null || ms < bucket._minStartedAtMs) {
217
+ bucket._minStartedAtMs = ms;
218
+ }
219
+ }
220
+ }
221
+ const completedAtRaw = doc.completedAt;
222
+ if (typeof completedAtRaw === 'string') {
223
+ const ms = new Date(completedAtRaw).getTime();
224
+ if (Number.isFinite(ms)) {
225
+ if (bucket._maxCompletedAtMs === null || ms > bucket._maxCompletedAtMs) {
226
+ bucket._maxCompletedAtMs = ms;
227
+ }
228
+ }
229
+ }
230
+ if (typeof doc.processingDurationMs === 'number' && doc.processingDurationMs > 0) {
231
+ bucket._aiActiveMs += doc.processingDurationMs;
232
+ bucket._aiActiveSeen = true;
233
+ }
234
+ if (status === 'success') {
235
+ bucket.succeeded += 1;
236
+ bucket._docIdsBySucceeded.add(documentId);
237
+ } else if (status === 'failed') {
238
+ bucket.failed += 1;
239
+ bucket._docIdsByFailed.add(documentId);
240
+ if (failureCode) {
241
+ bucket._failureCodes.set(failureCode, (bucket._failureCodes.get(failureCode) ?? 0) + 1);
242
+ }
243
+ } else if (status === 'pending') {
244
+ bucket.pending += 1;
245
+ bucket._docIdsByPending.add(documentId);
246
+ } else if (status === 'running') {
247
+ bucket.running += 1;
248
+ bucket._docIdsByRunning.add(documentId);
249
+ } else if (status === 'skipped') {
250
+ bucket.skipped += 1;
251
+ bucket._docIdsBySkipped.add(documentId);
252
+ } else if (status === 'reverted') {
253
+ bucket.reverted += 1;
254
+ bucket._docIdsByReverted.add(documentId);
255
+ }
256
+ }
257
+ if (!result.hasNextPage) break;
258
+ page += 1;
259
+ }
260
+ // Flatten + compute top-2 failure codes per bucket.
261
+ const flat = {};
262
+ for (const [collection, bucket] of Object.entries(out)){
263
+ const codeEntries = [
264
+ ...bucket._failureCodes.entries()
265
+ ];
266
+ const collectionSpanMs = bucket._minStartedAtMs !== null && bucket._maxCompletedAtMs !== null ? Math.max(0, bucket._maxCompletedAtMs - bucket._minStartedAtMs) : null;
267
+ const aiActiveMs = bucket._aiActiveSeen ? bucket._aiActiveMs : null;
268
+ // Queue wait is the gap between the editor-perceived wallclock and
269
+ // the actual AI compute time. Clamp to >=0 because aiActiveMs can
270
+ // exceed span when units ran in parallel across multiple workers.
271
+ const queueWaitMs = collectionSpanMs !== null && aiActiveMs !== null ? Math.max(0, collectionSpanMs - aiActiveMs) : null;
272
+ flat[collection] = {
273
+ isGlobal: bucket.isGlobal,
274
+ total: bucket.total,
275
+ pending: bucket.pending,
276
+ running: bucket.running,
277
+ succeeded: bucket.succeeded,
278
+ failed: bucket.failed,
279
+ skipped: bucket.skipped,
280
+ reverted: bucket.reverted,
281
+ docCountsByStatus: {
282
+ all: bucket._docIds.size,
283
+ pending: bucket._docIdsByPending.size,
284
+ running: bucket._docIdsByRunning.size,
285
+ completed: bucket._docIdsBySucceeded.size,
286
+ failed: bucket._docIdsByFailed.size,
287
+ skipped: bucket._docIdsBySkipped.size,
288
+ reverted: bucket._docIdsByReverted.size
289
+ },
290
+ totalDocs: bucket._docIds.size,
291
+ collectionSpanMs,
292
+ aiActiveMs,
293
+ queueWaitMs,
294
+ distinctFailureCodes: codeEntries.length,
295
+ topFailureCodes: codeEntries.sort((a, b)=>b[1] - a[1]).slice(0, 2).map(([code, count])=>({
296
+ code,
297
+ count
298
+ }))
299
+ };
300
+ }
301
+ return flat;
302
+ }
303
+ /**
304
+ * Type alias used by the aggregator above. Inlined here so the
305
+ * function signature stays self-documenting without a named export.
306
+ */ function aggregateUnitCountsByCollectionShape() {
307
+ return {
308
+ isGlobal: false,
309
+ total: 0,
310
+ pending: 0,
311
+ running: 0,
312
+ succeeded: 0,
313
+ failed: 0,
314
+ skipped: 0,
315
+ reverted: 0,
316
+ docCountsByStatus: {
317
+ all: 0,
318
+ pending: 0,
319
+ running: 0,
320
+ completed: 0,
321
+ failed: 0,
322
+ skipped: 0,
323
+ reverted: 0
324
+ },
325
+ totalDocs: 0,
326
+ collectionSpanMs: null,
327
+ aiActiveMs: null,
328
+ queueWaitMs: null,
329
+ distinctFailureCodes: 0,
330
+ topFailureCodes: []
331
+ };
332
+ }
333
+ async function fetchUnitPage(payload, unitsSlug, batchId, statusFilter, offset, limit, collectionFilter) {
334
+ const andClauses = [
335
+ {
336
+ batchId: {
337
+ equals: batchId
338
+ }
339
+ }
340
+ ];
341
+ if (statusFilter) {
342
+ // Map the public `completed` alias back onto storage's `success`.
343
+ const storageStatus = statusFilter === 'completed' ? 'success' : statusFilter;
344
+ andClauses.push({
345
+ status: {
346
+ equals: storageStatus
347
+ }
348
+ });
349
+ }
350
+ if (collectionFilter) {
351
+ // Used by the tree DrillDown's bucket-on-expand fetch to load just
352
+ // that collection's units rather than paging through every unit in
353
+ // the batch. The slug doubles as the global id, so this works for
354
+ // both collection and global buckets.
355
+ andClauses.push({
356
+ collection: {
357
+ equals: collectionFilter
358
+ }
359
+ });
360
+ }
361
+ // Payload doesn't expose an `offset`-based pagination today, so we
362
+ // compute the page number from offset+limit. This is correct so
363
+ // long as `limit` stays stable between requests — which is the
364
+ // contract of cursor-based pagination.
365
+ const page = Math.floor(offset / limit) + 1;
366
+ const result = await payload.find({
367
+ collection: unitsSlug,
368
+ where: {
369
+ and: andClauses
370
+ },
371
+ sort: 'createdAt',
372
+ page,
373
+ limit,
374
+ depth: 0,
375
+ overrideAccess: true
376
+ });
377
+ return result.docs.map((u)=>({
378
+ unitId: String(u.id),
379
+ collection: String(u.collection ?? ''),
380
+ documentId: String(u.documentId ?? ''),
381
+ locale: String(u.locale ?? ''),
382
+ status: String(u.status ?? ''),
383
+ attempts: Number(u.attempts ?? 0),
384
+ failureCode: typeof u.failureCode === 'string' ? u.failureCode : null,
385
+ failureMessage: typeof u.failureMessage === 'string' ? u.failureMessage : null,
386
+ costUsd: Number(u.costUsd ?? 0),
387
+ startedAt: typeof u.startedAt === 'string' ? u.startedAt : null,
388
+ completedAt: typeof u.completedAt === 'string' ? u.completedAt : null,
389
+ processingDurationMs: typeof u.processingDurationMs === 'number' ? u.processingDurationMs : null
390
+ }));
391
+ }
@@ -0,0 +1,114 @@
1
+ import type { PayloadHandler } from 'payload';
2
+ export type UsageSummaryRange = {
3
+ kind: '7d' | '30d' | '90d';
4
+ since: string;
5
+ } | {
6
+ kind: 'all';
7
+ } | {
8
+ kind: 'custom';
9
+ from?: string;
10
+ to?: string;
11
+ };
12
+ export interface UsageSummaryTotals {
13
+ runs: number;
14
+ /**
15
+ * ROUND2-2 (v1.2.6, fix round): canonical "did this run write any
16
+ * fields" definition — `status = 'succeeded' AND fieldsTranslated > 0`.
17
+ * Counts only runs that actually called the LLM and persisted at
18
+ * least one field. Manual-edit-preserve-only runs (status =
19
+ * 'succeeded' but fieldsTranslated = 0) are NOT counted here; they
20
+ * surface as `preserved` instead.
21
+ *
22
+ * Before this fix, Hub Overview counted bare `status = 'succeeded'`
23
+ * (which includes preserved-only no-ops) while Audit & Cost counted
24
+ * `fieldsTranslated > 0`. Same KPI named the same way reported two
25
+ * different numbers — the editor-trust failure mode NEW-1 was filed
26
+ * to prevent.
27
+ *
28
+ * Invariant: `runs === succeeded + failed + preserved`.
29
+ */
30
+ succeeded: number;
31
+ failed: number;
32
+ /**
33
+ * ROUND2-2: status = 'succeeded' AND fieldsTranslated <= 0. Manual-
34
+ * edit-guard preserves, hash-skip-only re-runs, etc. Surfaced as a
35
+ * separate count so editors can see "your translation didn't do
36
+ * nothing — it just had nothing new to write." Falls under "not
37
+ * failed" but isn't a write either.
38
+ */
39
+ preserved: number;
40
+ inputTokens: number;
41
+ outputTokens: number;
42
+ costUsd: number;
43
+ totalMatching: number;
44
+ /**
45
+ * Always `false` from this endpoint — the SQL aggregation always
46
+ * covers every row matching the window. The flag is preserved on the
47
+ * response so the UI's existing truncation chip code can keep
48
+ * checking it without branching. Client-side aggregation is gone.
49
+ */
50
+ truncated: boolean;
51
+ }
52
+ export interface UsageSummaryCollectionBucket {
53
+ slug: string;
54
+ kind: 'collection' | 'global';
55
+ runs: number;
56
+ tokens: number;
57
+ costUsd: number;
58
+ }
59
+ export interface UsageSummaryModelBucket {
60
+ /** Provider/model slug; may be the literal `'__unresolved__'` for
61
+ * rows that failed before model selection (see NEW-2). */
62
+ model: string;
63
+ runs: number;
64
+ tokens: number;
65
+ costUsd: number;
66
+ }
67
+ export interface UsageSummaryLocaleBucket {
68
+ locale: string;
69
+ runs: number;
70
+ failed: number;
71
+ }
72
+ export interface UsageSummarySampleRow {
73
+ id: number | string;
74
+ createdAt: string;
75
+ slug: string;
76
+ kind: 'collection' | 'global';
77
+ documentId?: string | null;
78
+ status: 'succeeded' | 'failed';
79
+ model?: string | null;
80
+ inputTokens: number;
81
+ outputTokens: number;
82
+ estimatedCostUsd?: number | null;
83
+ durationMs?: number | null;
84
+ error?: string | null;
85
+ failedCount: number;
86
+ succeededCount: number;
87
+ targetLocales?: Array<{
88
+ locale: string;
89
+ status?: 'succeeded' | 'failed' | null;
90
+ }> | null;
91
+ }
92
+ export interface UsageSummaryResponse {
93
+ range: UsageSummaryRange;
94
+ totals: UsageSummaryTotals;
95
+ byCollection: UsageSummaryCollectionBucket[];
96
+ byModel: UsageSummaryModelBucket[];
97
+ byLocale: UsageSummaryLocaleBucket[];
98
+ samples: UsageSummarySampleRow[];
99
+ }
100
+ export interface UsageSummaryHandlerOptions {
101
+ /** Override the usage collection slug. Defaults to `translation-usage`. */
102
+ usageCollectionSlug?: string;
103
+ }
104
+ /**
105
+ * Parse the `range`/`from`/`to` query params. Exported for testing —
106
+ * mirrors the StatusStrip/AuditPanel resolution rules so the UI and
107
+ * the endpoint agree on what "Last 7 days" means.
108
+ */
109
+ export declare function parseRangeParams(params: URLSearchParams): {
110
+ range: UsageSummaryRange;
111
+ since: string | null;
112
+ until: string | null;
113
+ };
114
+ export declare const getTranslationHubUsageSummaryHandler: (options?: UsageSummaryHandlerOptions) => PayloadHandler;