@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,233 @@
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 { getDrizzle, slugToTable } from '../../lib/bulk-translate-migrations.js';
4
+ import { createScopedLogger } from '../../lib/logger.js';
5
+ import { errorResponse, forbiddenOwnershipResponse, isEditorOrAdmin, ownsBatch, unauthorizedResponse } from './_helpers.js';
6
+ import { extractBatchId } from './status.js';
7
+ // ---------------------------------------------------------------------------
8
+ // Handler
9
+ // ---------------------------------------------------------------------------
10
+ const TERMINAL_STATUSES = new Set([
11
+ 'success',
12
+ 'partial',
13
+ 'failed',
14
+ 'cancelled',
15
+ 'reverted'
16
+ ]);
17
+ /**
18
+ * `POST /api/translation-hub/bulk-translate/:id/cancel`
19
+ *
20
+ * Transitions a queued/running batch to `cancelling`. The coordinator
21
+ * and worker tasks honor the new status at their next checkpoint
22
+ * (coordinator reads `batch.status` at the top of every tick; worker
23
+ * reads at terminal write). In-flight LLM calls finish naturally —
24
+ * we don't try to interrupt them mid-stream.
25
+ *
26
+ * Queued payload-jobs rows for this batch are best-effort cancelled
27
+ * here (we update them to a terminal state so the runner skips them).
28
+ * Some Payload versions expose a jobs API for this; we fall back to a
29
+ * direct update on the queued rows when the API isn't available.
30
+ */ export const getBulkTranslateCancelHandler = (options = {})=>async (req)=>{
31
+ const batchesSlug = options.batchesCollectionSlug ?? DEFAULT_BULK_TRANSLATE_BATCHES_COLLECTION_SLUG;
32
+ const unitsSlug = options.unitsCollectionSlug ?? DEFAULT_BULK_TRANSLATE_UNITS_COLLECTION_SLUG;
33
+ const jobsSlug = options.jobsCollectionSlug ?? 'payload-jobs';
34
+ if (!req.user) {
35
+ return unauthorizedResponse(req);
36
+ }
37
+ if (!isEditorOrAdmin(req.user)) {
38
+ return errorResponse('forbidden', "You don't have permission to cancel a bulk-translation batch. Contact an admin.", 403);
39
+ }
40
+ const batchId = extractBatchId(req.url ?? '');
41
+ if (!batchId) {
42
+ return errorResponse('invalid_batch_id', 'Batch ID could not be parsed from the request URL.', 400);
43
+ }
44
+ let batch;
45
+ try {
46
+ batch = await req.payload.findByID({
47
+ collection: batchesSlug,
48
+ id: batchId,
49
+ overrideAccess: true,
50
+ depth: 0
51
+ });
52
+ } catch (err) {
53
+ const log = createScopedLogger(req.payload, {
54
+ component: 'hub.cancel',
55
+ batchId
56
+ });
57
+ log.event('warn', 'hub.cancel.batch-read.failed', {
58
+ err,
59
+ endpoint: 'hub.cancel',
60
+ batchId
61
+ });
62
+ batch = undefined;
63
+ }
64
+ if (!batch) {
65
+ return errorResponse('not_found', 'This translation run no longer exists. Refresh the page.', 404);
66
+ }
67
+ // 1.2.8: editor can only cancel a run they triggered. Admins always
68
+ // pass (see `ownsBatch`). The check is intentionally AFTER the
69
+ // not_found case so editors can't probe for the existence of other
70
+ // editors' batches via 403-vs-404 timing.
71
+ if (!ownsBatch(req.user, batch)) {
72
+ return forbiddenOwnershipResponse();
73
+ }
74
+ if (TERMINAL_STATUSES.has(batch.status) || batch.status === 'cancelling') {
75
+ const isCancelling = batch.status === 'cancelling';
76
+ return errorResponse('invalid_state', isCancelling ? 'This run is already being cancelled — no action needed.' : "This run has already ended — there's nothing to cancel.", 409, {
77
+ status: batch.status
78
+ });
79
+ }
80
+ // ----- Flip status to cancelling -----
81
+ try {
82
+ await req.payload.update({
83
+ collection: batchesSlug,
84
+ id: batchId,
85
+ data: {
86
+ status: 'cancelling',
87
+ cancelledByUserId: String(req.user.id),
88
+ cancelledAt: new Date().toISOString()
89
+ },
90
+ overrideAccess: true
91
+ });
92
+ } catch (err) {
93
+ req.payload.logger?.warn?.(`[ai-translate] cancel: failed to mark batch cancelling for ${batchId}: ${err instanceof Error ? err.message : String(err)}`);
94
+ return errorResponse('cancel_failed', "The run couldn't be cancelled right now. Refresh the page and try again, or contact engineering.", 500);
95
+ }
96
+ // ----- Cancel queued payload-jobs rows for this batch -----
97
+ // We can't be surgical without a jsonb path query (see Spike 2);
98
+ // instead, we look up the units that are still `pending` (and have
99
+ // not yet been dequeued) and mark them as `skipped`. The worker
100
+ // task's first-action terminal-state guard makes any race here
101
+ // safe — if a worker did dequeue between our read + write, it
102
+ // sees the unit is no longer `pending` and exits.
103
+ let cancelledJobs = 0;
104
+ let inFlightJobs = 0;
105
+ try {
106
+ // Fast path: one set-based UPDATE on Postgres. The previous
107
+ // per-row loop held the HTTP request open for ~15ms × pending
108
+ // units — on a 10k-unit batch that's ~2.5 minutes, past every
109
+ // LB timeout, while units claimed mid-loop kept translating.
110
+ // The single statement is atomic w.r.t. the worker's claim
111
+ // (claim flips pending→running; whichever lands first wins and
112
+ // the other side sees a terminal/claimed row and backs off).
113
+ let sweptInBulk = false;
114
+ if (/^\d+$/.test(batchId)) {
115
+ const db = getDrizzle(req.payload);
116
+ if (db) {
117
+ try {
118
+ const res = await db.execute(`UPDATE ${slugToTable(unitsSlug)}
119
+ SET status = 'skipped',
120
+ failure_message = 'cancelled_by_admin',
121
+ completed_at = now(),
122
+ updated_at = now()
123
+ WHERE batch_id_id = ${batchId}
124
+ AND status = 'pending'`);
125
+ cancelledJobs = Number(res?.rowCount ?? 0);
126
+ sweptInBulk = true;
127
+ } catch (err) {
128
+ req.payload.logger?.warn?.(`[ai-translate] cancel: set-based pending sweep failed for batch ${batchId}, falling back to per-row: ${err instanceof Error ? err.message : String(err)}`);
129
+ }
130
+ }
131
+ }
132
+ if (!sweptInBulk) {
133
+ // Non-Postgres adapters / non-numeric ids: per-row fallback.
134
+ const pending = await req.payload.find({
135
+ collection: unitsSlug,
136
+ where: {
137
+ and: [
138
+ {
139
+ batchId: {
140
+ equals: batchId
141
+ }
142
+ },
143
+ {
144
+ status: {
145
+ equals: 'pending'
146
+ }
147
+ }
148
+ ]
149
+ },
150
+ limit: 10_000,
151
+ depth: 0,
152
+ overrideAccess: true
153
+ });
154
+ for (const unit of pending.docs){
155
+ try {
156
+ await req.payload.update({
157
+ collection: unitsSlug,
158
+ id: unit.id,
159
+ data: {
160
+ status: 'skipped',
161
+ failureMessage: 'cancelled_by_admin',
162
+ completedAt: new Date().toISOString()
163
+ },
164
+ overrideAccess: true
165
+ });
166
+ cancelledJobs += 1;
167
+ } catch {
168
+ // Per-unit failure — best-effort cancel.
169
+ }
170
+ }
171
+ }
172
+ const running = await req.payload.count({
173
+ collection: unitsSlug,
174
+ where: {
175
+ and: [
176
+ {
177
+ batchId: {
178
+ equals: batchId
179
+ }
180
+ },
181
+ {
182
+ status: {
183
+ equals: 'running'
184
+ }
185
+ }
186
+ ]
187
+ },
188
+ overrideAccess: true
189
+ });
190
+ inFlightJobs = running.totalDocs;
191
+ } catch (err) {
192
+ req.payload.logger?.warn?.(`[ai-translate] cancel: best-effort unit cleanup failed for batch ${batchId}: ${err instanceof Error ? err.message : String(err)}`);
193
+ }
194
+ // v1.2.5: short-circuit batch transition when there are no
195
+ // in-flight workers. Pre-1.2.5 the batch stayed in `cancelling`
196
+ // forever in this case — `maybeTransitionBatch` is fired by
197
+ // workers as they finish, but if 0 workers are running there's
198
+ // nothing to fire it. The result was a "stuck cancelling" state
199
+ // that only `/force-reset` could clear. Now we flip directly to
200
+ // `cancelled` here. When `inFlightJobs > 0` we leave the batch in
201
+ // `cancelling` and rely on the (now-fixed) `maybeTransitionBatch`
202
+ // path to flip it once the last in-flight unit lands.
203
+ if (inFlightJobs === 0) {
204
+ try {
205
+ await req.payload.update({
206
+ collection: batchesSlug,
207
+ id: batchId,
208
+ data: {
209
+ status: 'cancelled',
210
+ completedAt: new Date().toISOString()
211
+ },
212
+ overrideAccess: true
213
+ });
214
+ } catch (err) {
215
+ // Best-effort. If this fails, the batch stays in `cancelling`
216
+ // and force-reset is still the manual escape hatch.
217
+ req.payload.logger?.warn?.(`[ai-translate] cancel: short-circuit batch transition failed for ${batchId}: ${err instanceof Error ? err.message : String(err)}`);
218
+ }
219
+ }
220
+ // Try (best-effort) to drop queued payload-jobs rows for the
221
+ // coordinator/worker for this batch. The exact column layout
222
+ // varies across Payload versions, so we wrap the optional path
223
+ // in a try.
224
+ void jobsSlug; // reserved for future use; see comment above.
225
+ return Response.json({
226
+ data: {
227
+ batchId,
228
+ cancelledJobs,
229
+ inFlightJobs,
230
+ status: inFlightJobs === 0 ? 'cancelled' : 'cancelling'
231
+ }
232
+ });
233
+ };
@@ -0,0 +1,70 @@
1
+ import type { PayloadHandler } from 'payload';
2
+ import type { AITranslatePluginConfig, BulkTranslateConfig } from '../../types.js';
3
+ export interface BulkEnqueueScope {
4
+ collections?: string[];
5
+ globals?: string[];
6
+ locales?: string[];
7
+ excludeCollections?: string[];
8
+ documentIds?: Record<string, string[]>;
9
+ }
10
+ export interface BulkEnqueueBody {
11
+ scope: BulkEnqueueScope;
12
+ mode: 'changed' | 'force' | 'canary';
13
+ canaryLimit?: number;
14
+ totpCode?: string;
15
+ triggerReason?: string;
16
+ }
17
+ export interface BulkEnqueueHandlerOptions {
18
+ /** Slug override for the batches collection. */
19
+ batchesCollectionSlug?: string;
20
+ /** Coordinator task slug to enqueue. */
21
+ coordinatorTaskSlug?: string;
22
+ }
23
+ /**
24
+ * `POST /api/translation-hub/bulk-translate` — enqueue a bulk run.
25
+ *
26
+ * Flow (see Design-2026-05-27-bulk-translate.md §4 PR2):
27
+ * 1. Auth — admin role (Decision #31).
28
+ * 2. TOTP friction — gated by `bulk.requireTotp` (Decision #13 v2).
29
+ * Friction-not-boundary: the daily USD cap is the real guard.
30
+ * 3. Body validation — discriminated by `mode`.
31
+ * 4. Concurrency refusal — one bulk run at a time per Decision #2.
32
+ * 5. Cost estimate — sum provider's `estimate()` across the resolved
33
+ * doc list. Hard-reject if `undefined` (Decision #29). Canary
34
+ * mode skips estimation — too cheap to matter.
35
+ * 6. Daily cap — `checkAndIncrementDailySpend` (Decision #14).
36
+ * 7. Snapshot pinned config — `{ providerKey, modelId, sourceLocale }`
37
+ * captured at enqueue (Decision #5).
38
+ * 8. Create batch row → queue coordinator task → 202.
39
+ */
40
+ export declare const getBulkTranslateEnqueueHandler: (options?: BulkEnqueueHandlerOptions) => PayloadHandler;
41
+ type ResolvedScope = {
42
+ collections: string[];
43
+ globals: string[];
44
+ locales: string[];
45
+ excludeCollections: string[];
46
+ /**
47
+ * The coordinator's `BulkBatchScope.documentIds` is a flat array
48
+ * (it only enumerates a single collection in `documents-explicit`
49
+ * mode). When the caller supplies a per-collection map, we flatten
50
+ * here AFTER honoring `collections` membership.
51
+ */
52
+ documentIdsFlat: string[];
53
+ };
54
+ export declare function resolveScope(scope: BulkEnqueueScope, config: AITranslatePluginConfig, bulk: BulkTranslateConfig): ResolvedScope;
55
+ export type BatchCostEstimate = {
56
+ estimatedCostUsd: number | undefined;
57
+ documentCount: number;
58
+ };
59
+ export type BatchEstimateScope = ResolvedScope;
60
+ /**
61
+ * Enumerate the scope's docs, run the provider's `estimate()` once
62
+ * with the aggregated items, multiply by locale count. Mirrors the
63
+ * pattern in `translate.ts:estimateRequestCost` but at batch scale.
64
+ *
65
+ * Returns `estimatedCostUsd: undefined` only when the provider has no
66
+ * `estimate()` method — the enqueue handler then hard-rejects per
67
+ * Decision #29.
68
+ */
69
+ export declare function estimateBatchCost(payload: import('payload').Payload, config: AITranslatePluginConfig, scope: ResolvedScope): Promise<BatchCostEstimate>;
70
+ export {};