@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,204 @@
1
+ 'use client';
2
+ /**
3
+ * Polling hook for the bulk-translate runs list endpoint.
4
+ *
5
+ * Mirrors the structure of `useBulkTranslateActive` — 5s visibility-aware
6
+ * poll loop, offline threshold, refetch escape hatch.
7
+ *
8
+ * Polling is active only when:
9
+ * 1. `document.visibilityState === 'visible'`
10
+ * 2. Any batch in `data.batches` is in an active status.
11
+ *
12
+ * When those conditions are false the poll is suspended. The hook still
13
+ * fires once on mount and once when visibility returns.
14
+ */ import { useCallback, useEffect, useRef, useState } from 'react';
15
+ import { readResponseError } from '../shared/fetch-error-body.js';
16
+ import { isActiveStatus } from '../TranslationHub/BulkTranslate.types.js';
17
+ const POLL_INTERVAL_MS = 5000;
18
+ const OFFLINE_THRESHOLD = 3;
19
+ const DEFAULT_LIMIT = 20;
20
+ function buildUrl(basePath, filters, cursor = null, limit = DEFAULT_LIMIT) {
21
+ const params = new URLSearchParams();
22
+ if (filters.status) params.set('status', filters.status);
23
+ if (filters.mode) params.set('mode', filters.mode);
24
+ if (filters.triggeredBy) params.set('triggeredBy', filters.triggeredBy);
25
+ if (filters.since) params.set('since', filters.since);
26
+ if (filters.until) params.set('until', filters.until);
27
+ if (filters.hasFailures) params.set('hasFailures', 'true');
28
+ if (cursor) params.set('cursor', cursor);
29
+ params.set('limit', String(limit));
30
+ // Payload's collection-list router intercepts requests whose only query
31
+ // string is `?limit=N`, redirecting to a locale-prefixed canonical URL
32
+ // and 404ing on our custom endpoint. Adding a second param sidesteps
33
+ // the heuristic. `depth=0` is a no-op for our handler.
34
+ params.set('depth', '0');
35
+ const qs = params.toString();
36
+ return `${basePath}/api/translation-hub/bulk-translate${qs ? `?${qs}` : ''}`;
37
+ }
38
+ export function useBulkRunsList(basePath, filters) {
39
+ const [data, setData] = useState(null);
40
+ const [loading, setLoading] = useState(true);
41
+ const [error, setError] = useState(null);
42
+ const [failedStreak, setFailedStreak] = useState(0);
43
+ const [loadingMore, setLoadingMore] = useState(false);
44
+ const cancelledRef = useRef(false);
45
+ const timerRef = useRef(undefined);
46
+ // Stable ref so refetch() can be called from event handlers without
47
+ // re-subscribing to the effect.
48
+ const refetchRef = useRef(()=>undefined);
49
+ useEffect(()=>{
50
+ cancelledRef.current = false;
51
+ let isFirstFetch = true;
52
+ async function fetchOnce() {
53
+ const url = buildUrl(basePath, filters);
54
+ try {
55
+ const res = await fetch(url, {
56
+ credentials: 'include'
57
+ });
58
+ if (cancelledRef.current) return;
59
+ if (res.status === 404) {
60
+ setData({
61
+ batches: [],
62
+ nextCursor: null,
63
+ total: 0
64
+ });
65
+ setError(null);
66
+ setFailedStreak(0);
67
+ return;
68
+ }
69
+ if (!res.ok) throw new Error(await readResponseError(res));
70
+ const json = await res.json();
71
+ if (cancelledRef.current) return;
72
+ // v1.2.5: same fix as BatchRow's polling — preserve
73
+ // load-more-appended batches across polling ticks. Pre-1.2.5
74
+ // the polling tick called setData(json) which fully replaced
75
+ // the batches array; if the user had clicked "Load more" to
76
+ // load page 2, the next polling tick wiped page 2 (the
77
+ // polling fetch returns only the first page). Now we merge:
78
+ // new batches override matching ids, prev batches not in the
79
+ // new response (i.e. loaded-more pages) survive. First fetch
80
+ // (filter change / mount) still replaces fully.
81
+ if (isFirstFetch) {
82
+ setData(json);
83
+ isFirstFetch = false;
84
+ } else {
85
+ setData((prev)=>{
86
+ if (!prev) return json;
87
+ const newById = new Map(json.batches.map((b)=>[
88
+ String(b.id),
89
+ b
90
+ ]));
91
+ const preserved = prev.batches.filter((b)=>!newById.has(String(b.id)));
92
+ return {
93
+ ...json,
94
+ batches: [
95
+ ...json.batches,
96
+ ...preserved
97
+ ]
98
+ };
99
+ });
100
+ }
101
+ setError(null);
102
+ setFailedStreak(0);
103
+ } catch (e) {
104
+ if (cancelledRef.current) return;
105
+ setError(e instanceof Error ? e.message : String(e));
106
+ setFailedStreak((n)=>n + 1);
107
+ } finally{
108
+ if (!cancelledRef.current) {
109
+ setLoading(false);
110
+ // Only keep polling when page is visible AND a batch is active.
111
+ if (document.visibilityState === 'visible') {
112
+ // We can't read `data` here (stale closure) so we schedule
113
+ // unconditionally; the interval handler checks activity itself.
114
+ timerRef.current = setTimeout(maybePoll, POLL_INTERVAL_MS);
115
+ }
116
+ }
117
+ }
118
+ }
119
+ function maybePoll() {
120
+ if (cancelledRef.current) return;
121
+ // Read latest data from the outer state via the ref we refresh below.
122
+ // If none of the current batches are active, suspend polling.
123
+ const shouldPoll = dataRef.current?.batches.some((b)=>isActiveStatus(b.status));
124
+ if (shouldPoll) {
125
+ fetchOnce();
126
+ } else {
127
+ // Reschedule — a new run might start while the tab is open.
128
+ timerRef.current = setTimeout(maybePoll, POLL_INTERVAL_MS);
129
+ }
130
+ }
131
+ function onVisibility() {
132
+ if (document.visibilityState === 'visible' && !cancelledRef.current) {
133
+ if (timerRef.current) clearTimeout(timerRef.current);
134
+ fetchOnce();
135
+ }
136
+ }
137
+ refetchRef.current = ()=>{
138
+ if (timerRef.current) clearTimeout(timerRef.current);
139
+ fetchOnce();
140
+ };
141
+ document.addEventListener('visibilitychange', onVisibility);
142
+ fetchOnce();
143
+ return ()=>{
144
+ cancelledRef.current = true;
145
+ if (timerRef.current) clearTimeout(timerRef.current);
146
+ document.removeEventListener('visibilitychange', onVisibility);
147
+ };
148
+ // eslint-disable-next-line react-hooks/exhaustive-deps
149
+ }, [
150
+ basePath,
151
+ filters.status,
152
+ filters.mode,
153
+ filters.triggeredBy,
154
+ filters.since,
155
+ filters.until,
156
+ filters.hasFailures
157
+ ]);
158
+ // Keep a ref to latest data so `maybePoll` doesn't stale-close.
159
+ const dataRef = useRef(data);
160
+ useEffect(()=>{
161
+ dataRef.current = data;
162
+ }, [
163
+ data
164
+ ]);
165
+ const loadMore = useCallback(async ()=>{
166
+ const cursor = dataRef.current?.nextCursor;
167
+ if (!cursor || loadingMore) return;
168
+ setLoadingMore(true);
169
+ try {
170
+ const url = buildUrl(basePath, filters, cursor);
171
+ const res = await fetch(url, {
172
+ credentials: 'include'
173
+ });
174
+ if (!res.ok) throw new Error(await readResponseError(res));
175
+ const json = await res.json();
176
+ setData((prev)=>prev ? {
177
+ ...json,
178
+ batches: [
179
+ ...prev.batches,
180
+ ...json.batches
181
+ ]
182
+ } : json);
183
+ } catch {
184
+ // Non-fatal — the Load more button just stays visible.
185
+ } finally{
186
+ setLoadingMore(false);
187
+ }
188
+ }, [
189
+ basePath,
190
+ filters,
191
+ loadingMore
192
+ ]);
193
+ return {
194
+ data,
195
+ loading,
196
+ error,
197
+ isOffline: failedStreak >= OFFLINE_THRESHOLD,
198
+ refetch: ()=>refetchRef.current(),
199
+ loadMore,
200
+ loadingMore
201
+ };
202
+ }
203
+ export const BULK_RUNS_OFFLINE_THRESHOLD = OFFLINE_THRESHOLD;
204
+ export const BULK_RUNS_POLL_INTERVAL_MS = POLL_INTERVAL_MS;
@@ -0,0 +1,10 @@
1
+ import type { BulkRunsFilterState, BulkRunsTimeRange } from '../TranslationHub/BulkTranslate.types.js';
2
+ export { DEFAULT_FILTERS } from './urlFilters.js';
3
+ export type UrlFiltersResult = {
4
+ filters: BulkRunsFilterState;
5
+ timeRange: BulkRunsTimeRange;
6
+ setFilters: (patch: Partial<BulkRunsFilterState>) => void;
7
+ setTimeRange: (range: BulkRunsTimeRange) => void;
8
+ clearAll: () => void;
9
+ };
10
+ export declare function useUrlFilters(): UrlFiltersResult;
@@ -0,0 +1,88 @@
1
+ 'use client';
2
+ /**
3
+ * Sync the BulkRunsHub filter + timeRange state with the URL search
4
+ * params so the filtered view is refresh-stable and shareable.
5
+ *
6
+ * BR-7 (v1.2.6): before the fix, every filter (status pill, mode pill,
7
+ * since, until, hasFailures, triggeredBy) lived in React state only.
8
+ * Refresh or share-the-link dropped the filter; editors couldn't send
9
+ * "failed runs in May" as a URL.
10
+ *
11
+ * Contract:
12
+ * - The hook owns READ + WRITE of the filter / timeRange tuple. The
13
+ * component holds NO local React state for these — it reads from
14
+ * the hook and writes back via the returned setters.
15
+ * - URL writes use `router.replace(..., { scroll: false })` so the
16
+ * browser doesn't push a history entry per pill click and so the
17
+ * page doesn't jump to the top on every state update.
18
+ * - Empty / default values are omitted from the URL to keep it readable
19
+ * (`?status=failed&since=2026-05-01` is friendlier than the same
20
+ * plus six empty params).
21
+ * - SSR-safe: when `usePathname()` / `useSearchParams()` return null
22
+ * (first render in some App Router edge cases), we fall back to the
23
+ * defaults so the table doesn't render with `undefined` filter state.
24
+ *
25
+ * The pure serializers live in `./urlFilters.ts` so they can be unit
26
+ * tested without dragging `next/navigation` into the node test
27
+ * environment.
28
+ */ import { usePathname, useRouter, useSearchParams } from 'next/navigation';
29
+ import { useCallback, useMemo } from 'react';
30
+ import { buildSearchString, DEFAULT_FILTERS, readFilters, readTimeRange } from './urlFilters.js';
31
+ export { DEFAULT_FILTERS } from './urlFilters.js';
32
+ export function useUrlFilters() {
33
+ const router = useRouter();
34
+ const pathname = usePathname();
35
+ const searchParams = useSearchParams();
36
+ const filters = useMemo(()=>{
37
+ return readFilters(searchParams ? new URLSearchParams(searchParams.toString()) : new URLSearchParams());
38
+ }, [
39
+ searchParams
40
+ ]);
41
+ const timeRange = useMemo(()=>{
42
+ return readTimeRange(searchParams ? new URLSearchParams(searchParams.toString()) : new URLSearchParams());
43
+ }, [
44
+ searchParams
45
+ ]);
46
+ const writeUrl = useCallback((nextFilters, nextRange)=>{
47
+ const qs = buildSearchString(nextFilters, nextRange);
48
+ // `pathname` can be null in some App Router edge cases (e.g. when
49
+ // the hook fires before the router context is ready). Skip the
50
+ // navigation in that case — next render will hit the path branch.
51
+ if (!pathname) return;
52
+ router.replace(`${pathname}${qs}`, {
53
+ scroll: false
54
+ });
55
+ }, [
56
+ pathname,
57
+ router
58
+ ]);
59
+ const setFilters = useCallback((patch)=>{
60
+ const next = {
61
+ ...filters,
62
+ ...patch
63
+ };
64
+ writeUrl(next, timeRange);
65
+ }, [
66
+ filters,
67
+ timeRange,
68
+ writeUrl
69
+ ]);
70
+ const setTimeRange = useCallback((range)=>{
71
+ writeUrl(filters, range);
72
+ }, [
73
+ filters,
74
+ writeUrl
75
+ ]);
76
+ const clearAll = useCallback(()=>{
77
+ writeUrl(DEFAULT_FILTERS, '7d');
78
+ }, [
79
+ writeUrl
80
+ ]);
81
+ return {
82
+ filters,
83
+ timeRange,
84
+ setFilters,
85
+ setTimeRange,
86
+ clearAll
87
+ };
88
+ }
@@ -0,0 +1,6 @@
1
+ import type React from 'react';
2
+ interface Props {
3
+ basePath: string;
4
+ }
5
+ export declare const ActiveJobs: React.FC<Props>;
6
+ export {};
@@ -0,0 +1,320 @@
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 { readResponseError } from '../shared/fetch-error-body.js';
5
+ // Active polling cadence — used while at least one job is `running`.
6
+ // 5s matches the "refresh every 5s" copy in the header.
7
+ const POLL_INTERVAL_MS = 5000;
8
+ // Idle cadence — used when no jobs are running. Pre-1.2.8 the loop
9
+ // re-queued at POLL_INTERVAL_MS unconditionally, so the page hammered
10
+ // `/api/ai-translate-jobs` every 5s for the entire time the Hub was
11
+ // open — even hours after the last job finished. Under a bulk run with
12
+ // many concurrent admin tabs that crashed the server. We still need to
13
+ // poll when idle (a job can be triggered from another tab and the
14
+ // In-flight panel must reflect it), but the cadence can be much
15
+ // slower.
16
+ const IDLE_POLL_INTERVAL_MS = 60_000;
17
+ const SECTION_STYLE = {
18
+ background: 'var(--theme-elevation-50)',
19
+ border: '1px solid var(--theme-elevation-150)',
20
+ borderRadius: '6px',
21
+ padding: '1rem'
22
+ };
23
+ const ROW_STYLE = {
24
+ display: 'grid',
25
+ gridTemplateColumns: '1fr 0.7fr 1.2fr 0.8fr 0.6fr',
26
+ gap: '0.75rem',
27
+ padding: '0.5rem 0',
28
+ alignItems: 'center',
29
+ fontSize: '0.875rem',
30
+ borderTop: '1px solid var(--theme-elevation-100)',
31
+ color: 'var(--theme-elevation-800)'
32
+ };
33
+ const HEADER_ROW_STYLE = {
34
+ ...ROW_STYLE,
35
+ borderTop: 'none',
36
+ fontSize: '0.75rem',
37
+ fontWeight: 600,
38
+ textTransform: 'uppercase',
39
+ letterSpacing: '0.05em',
40
+ color: 'var(--theme-elevation-500)'
41
+ };
42
+ const STATUS_COLORS = {
43
+ running: 'var(--theme-warning-500, #d97706)',
44
+ completed: 'var(--theme-success-500, #16a34a)',
45
+ failed: 'var(--theme-error-500, #b91c1c)',
46
+ stale: 'var(--theme-elevation-500)'
47
+ };
48
+ // Threshold for treating a `running` job as stuck/zombied. The cms-plugins
49
+ // persistJobs feature writes a row when a job starts but doesn't sweep it
50
+ // on server-restart, so a translate that crashed mid-flight stays
51
+ // `running` in the DB forever. Operationally any real translate finishes
52
+ // in seconds; anything over 5 minutes is dead.
53
+ const STALE_AFTER_MS = 5 * 60 * 1000;
54
+ function effectiveStatus(j) {
55
+ if (j.status !== 'running') {
56
+ return j.status;
57
+ }
58
+ const startedMs = new Date(j.startedAt ?? j.createdAt).getTime();
59
+ return Date.now() - startedMs > STALE_AFTER_MS ? 'stale' : 'running';
60
+ }
61
+ // Builds the jobs-list URL for the last 15-minute window. Extracted to
62
+ // keep `fetchOnce` under biome's cognitive-complexity ceiling.
63
+ function buildJobsUrl(basePath) {
64
+ const since = new Date(Date.now() - 15 * 60 * 1000).toISOString();
65
+ // Bumped from 10 → 50 — during bulk-publish bursts more than 10 jobs
66
+ // can be in flight concurrently. Without headroom the in-flight
67
+ // panel quietly drops older `running` jobs.
68
+ const params = new URLSearchParams({
69
+ 'where[createdAt][greater_than]': since,
70
+ limit: '50',
71
+ sort: '-createdAt',
72
+ depth: '0'
73
+ });
74
+ return `${basePath}/api/ai-translate-jobs?${params.toString()}`;
75
+ }
76
+ function relTime(iso) {
77
+ const diff = Date.now() - new Date(iso).getTime();
78
+ const s = Math.floor(diff / 1000);
79
+ if (s < 60) {
80
+ return `${s}s ago`;
81
+ }
82
+ if (s < 3600) {
83
+ return `${Math.floor(s / 60)}m ago`;
84
+ }
85
+ if (s < 86_400) {
86
+ return `${Math.floor(s / 3600)}h ago`;
87
+ }
88
+ return `${Math.floor(s / 86_400)}d ago`;
89
+ }
90
+ export const ActiveJobs = ({ basePath })=>{
91
+ const [jobs, setJobs] = useState(null);
92
+ const [error, setError] = useState(null);
93
+ useEffect(()=>{
94
+ let cancelled = false;
95
+ let timer;
96
+ // Tracks whether the most recent poll saw any `running` job. The
97
+ // next setTimeout uses this to pick the active vs idle cadence.
98
+ let hasActiveJob = false;
99
+ async function fetchJobs() {
100
+ const res = await fetch(buildJobsUrl(basePath), {
101
+ credentials: 'include'
102
+ });
103
+ if (!res.ok) {
104
+ throw new Error(await readResponseError(res));
105
+ }
106
+ const data = await res.json();
107
+ return data.docs ?? [];
108
+ }
109
+ async function fetchOnce() {
110
+ try {
111
+ const docs = await fetchJobs();
112
+ if (cancelled) {
113
+ return;
114
+ }
115
+ setJobs(docs);
116
+ setError(null);
117
+ hasActiveJob = docs.some((j)=>j.status === 'running');
118
+ } catch (e) {
119
+ if (!cancelled) {
120
+ setError(e instanceof Error ? e.message : String(e));
121
+ }
122
+ } finally{
123
+ // Visibility-aware polling: skip polling when the tab is
124
+ // hidden — `fetchOnce` re-fires from `onVisibility` below when
125
+ // the tab returns to foreground.
126
+ //
127
+ // Cadence-aware polling: 5s while a job is in flight, 60s once
128
+ // every job has terminated. Avoids hammering the endpoint
129
+ // forever while the Hub sits idle.
130
+ if (!cancelled && document.visibilityState === 'visible') {
131
+ const nextDelay = hasActiveJob ? POLL_INTERVAL_MS : IDLE_POLL_INTERVAL_MS;
132
+ timer = setTimeout(fetchOnce, nextDelay);
133
+ }
134
+ }
135
+ }
136
+ function onVisibility() {
137
+ if (document.visibilityState === 'visible' && !cancelled) {
138
+ // Tab came back to foreground — fire one fresh poll and the
139
+ // chain re-starts from there.
140
+ if (timer) {
141
+ clearTimeout(timer);
142
+ }
143
+ fetchOnce();
144
+ }
145
+ }
146
+ document.addEventListener('visibilitychange', onVisibility);
147
+ fetchOnce();
148
+ return ()=>{
149
+ cancelled = true;
150
+ if (timer) {
151
+ clearTimeout(timer);
152
+ }
153
+ document.removeEventListener('visibilitychange', onVisibility);
154
+ };
155
+ }, [
156
+ basePath
157
+ ]);
158
+ const running = jobs?.filter((j)=>effectiveStatus(j) === 'running') ?? [];
159
+ const stale = jobs?.filter((j)=>effectiveStatus(j) === 'stale') ?? [];
160
+ const recent = jobs ?? [];
161
+ return /*#__PURE__*/ _jsxs("section", {
162
+ style: SECTION_STYLE,
163
+ children: [
164
+ /*#__PURE__*/ _jsxs("header", {
165
+ style: {
166
+ display: 'flex',
167
+ alignItems: 'center',
168
+ justifyContent: 'space-between',
169
+ marginBottom: '0.75rem'
170
+ },
171
+ children: [
172
+ /*#__PURE__*/ _jsx("h2", {
173
+ style: {
174
+ margin: 0,
175
+ fontSize: '1rem',
176
+ color: 'var(--theme-elevation-1000)'
177
+ },
178
+ children: "In-flight"
179
+ }),
180
+ /*#__PURE__*/ _jsx("span", {
181
+ style: {
182
+ fontSize: '0.75rem',
183
+ color: 'var(--theme-elevation-500)'
184
+ },
185
+ children: (()=>{
186
+ if (jobs === null) {
187
+ return 'Loading…';
188
+ }
189
+ // Cadence text mirrors the actual poll interval so the user
190
+ // isn't misled into thinking we're hammering the server.
191
+ // While idle (no `running` jobs) we back off to 60s; the
192
+ // first newly-enqueued job is picked up by the next tick.
193
+ const cadence = running.length > 0 ? '5s' : '60s';
194
+ if (stale.length > 0) {
195
+ return `${running.length} running · ${stale.length} stale · refresh every ${cadence}`;
196
+ }
197
+ return `${running.length} running · refresh every ${cadence}`;
198
+ })()
199
+ })
200
+ ]
201
+ }),
202
+ error && /*#__PURE__*/ _jsx("p", {
203
+ style: {
204
+ color: 'var(--theme-error-500, #b91c1c)',
205
+ fontSize: '0.875rem',
206
+ margin: '0 0 0.5rem'
207
+ },
208
+ children: error
209
+ }),
210
+ recent.length === 0 ? /*#__PURE__*/ _jsx("p", {
211
+ style: {
212
+ margin: 0,
213
+ color: 'var(--theme-elevation-500)',
214
+ fontSize: '0.875rem'
215
+ },
216
+ children: "Nothing in flight. Translations finish in seconds; historical runs live in the Recent translations table below."
217
+ }) : /*#__PURE__*/ _jsxs(_Fragment, {
218
+ children: [
219
+ /*#__PURE__*/ _jsxs("div", {
220
+ style: HEADER_ROW_STYLE,
221
+ children: [
222
+ /*#__PURE__*/ _jsx("span", {
223
+ children: "Collection / doc"
224
+ }),
225
+ /*#__PURE__*/ _jsx("span", {
226
+ children: "Status"
227
+ }),
228
+ /*#__PURE__*/ _jsx("span", {
229
+ children: "Progress"
230
+ }),
231
+ /*#__PURE__*/ _jsx("span", {
232
+ children: "Started"
233
+ }),
234
+ /*#__PURE__*/ _jsx("span", {
235
+ style: {
236
+ textAlign: 'right'
237
+ },
238
+ children: "Locales"
239
+ })
240
+ ]
241
+ }),
242
+ recent.map((j)=>{
243
+ const completed = Array.isArray(j.completedLocales) ? j.completedLocales.length : 0;
244
+ const failed = Array.isArray(j.failedLocales) ? j.failedLocales.length : 0;
245
+ const progress = `${completed}/${j.totalLocales}`;
246
+ const eff = effectiveStatus(j);
247
+ return /*#__PURE__*/ _jsxs("div", {
248
+ style: ROW_STYLE,
249
+ children: [
250
+ /*#__PURE__*/ _jsxs("span", {
251
+ style: {
252
+ overflow: 'hidden',
253
+ textOverflow: 'ellipsis'
254
+ },
255
+ children: [
256
+ /*#__PURE__*/ _jsx("code", {
257
+ style: {
258
+ fontSize: '0.75rem'
259
+ },
260
+ children: j.collection
261
+ }),
262
+ ' ',
263
+ /*#__PURE__*/ _jsxs("span", {
264
+ style: {
265
+ color: 'var(--theme-elevation-500)'
266
+ },
267
+ children: [
268
+ "#",
269
+ j.documentId
270
+ ]
271
+ })
272
+ ]
273
+ }),
274
+ /*#__PURE__*/ _jsxs("span", {
275
+ style: {
276
+ color: STATUS_COLORS[eff],
277
+ fontWeight: 500
278
+ },
279
+ children: [
280
+ eff === 'stale' ? 'stale (server died)' : eff,
281
+ failed > 0 ? ` (${failed} failed)` : ''
282
+ ]
283
+ }),
284
+ /*#__PURE__*/ _jsxs("span", {
285
+ children: [
286
+ progress,
287
+ Array.isArray(j.completedLocales) && j.completedLocales.length > 0 && /*#__PURE__*/ _jsxs("span", {
288
+ style: {
289
+ marginLeft: '0.5rem',
290
+ color: 'var(--theme-elevation-500)',
291
+ fontSize: '0.75rem'
292
+ },
293
+ children: [
294
+ "(",
295
+ j.completedLocales.join(', '),
296
+ ")"
297
+ ]
298
+ })
299
+ ]
300
+ }),
301
+ /*#__PURE__*/ _jsx("span", {
302
+ style: {
303
+ color: 'var(--theme-elevation-500)'
304
+ },
305
+ children: relTime(j.startedAt ?? j.createdAt)
306
+ }),
307
+ /*#__PURE__*/ _jsx("span", {
308
+ style: {
309
+ textAlign: 'right'
310
+ },
311
+ children: j.totalLocales
312
+ })
313
+ ]
314
+ }, j.id);
315
+ })
316
+ ]
317
+ })
318
+ ]
319
+ });
320
+ };
@@ -0,0 +1,17 @@
1
+ import type React from 'react';
2
+ /**
3
+ * Advanced tab content. Real diagnostic and developer surfaces, not just
4
+ * a link dump. Aimed at the admin trying to answer:
5
+ * - "Is the plugin healthy right now?"
6
+ * - "How big are the bookkeeping tables?"
7
+ * - "Are there stale running jobs from a crashed worker?"
8
+ * - "How do I force re-translate everything ignoring the hash skip?"
9
+ *
10
+ * Pulls live counts from the various plugin-owned collections (only
11
+ * totals — no row content — to keep this cheap).
12
+ */
13
+ interface Props {
14
+ basePath: string;
15
+ }
16
+ export declare const AdvancedPanel: React.FC<Props>;
17
+ export default AdvancedPanel;