@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,6 @@
1
+ import type React from 'react';
2
+ interface Props {
3
+ basePath: string;
4
+ }
5
+ export declare const AlertBanner: React.FC<Props>;
6
+ export {};
@@ -0,0 +1,568 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useEffect, useMemo, useState } from 'react';
4
+ import { editorCodeFromAlertType, editorMessageFor } from '../../lib/error-messages.js';
5
+ import { docHref } from '../shared/docHref.js';
6
+ import { groupAlerts, splitVisibleGroups } from './alertGrouping.js';
7
+ const ALERT_LOOKBACK_MS = 24 * 60 * 60 * 1000; // 24h
8
+ // Active poll cadence — used while ≥1 undismissed alert is showing.
9
+ // 30s keeps the visible banner stack reasonably fresh as new alerts
10
+ // fire mid-translation without thrashing the endpoint.
11
+ const POLL_INTERVAL_MS = 30_000;
12
+ // Idle cadence — used when the latest poll returned zero alerts.
13
+ // Pre-1.2.8 the loop re-queued at POLL_INTERVAL_MS unconditionally
14
+ // (no visibility check, no idle back-off), so any admin tab on
15
+ // `/admin/translation` hit `/api/translation-alerts?where[...]=...`
16
+ // every 30s for the lifetime of the page — even after every alert
17
+ // was dismissed. Matches the same defect class fixed in
18
+ // `ActiveJobs.tsx` and `useBulkTranslateActive.ts`.
19
+ const IDLE_POLL_INTERVAL_MS = 120_000;
20
+ /** v1.2.6 HUB-1: bumped from 10 to 30 so the grouping has the data it
21
+ * needs to summarise larger bursts. The visible stack is capped at 3
22
+ * groups via `splitVisibleGroups`. */ const ALERT_PAGE_LIMIT = 30;
23
+ const MAX_VISIBLE_GROUPS = 3;
24
+ const ALERT_COLORS = {
25
+ 'cost-guard-abort': {
26
+ bg: 'var(--theme-warning-100, #fef3c7)',
27
+ border: 'var(--theme-warning-500, #d97706)',
28
+ fg: 'var(--theme-warning-500, #d97706)'
29
+ },
30
+ 'persistent-failure': {
31
+ bg: 'var(--theme-error-100, #fee2e2)',
32
+ border: 'var(--theme-error-500, #b91c1c)',
33
+ fg: 'var(--theme-error-500, #b91c1c)'
34
+ },
35
+ 'provider-outage': {
36
+ bg: 'var(--theme-error-100, #fee2e2)',
37
+ border: 'var(--theme-error-500, #b91c1c)',
38
+ fg: 'var(--theme-error-500, #b91c1c)'
39
+ }
40
+ };
41
+ /**
42
+ * Pull the editor-facing code + context off the persisted alert row.
43
+ * The catalogue's `editorMessageFor(code, context)` renders the
44
+ * friendly title + body. Falls back to the `alertType` mapping when
45
+ * `metadata.code` is absent (pre-1.2.5 rows).
46
+ */ function readAlertMessage(alert) {
47
+ const meta = alert.metadata ?? {};
48
+ const code = typeof meta['code'] === 'string' ? meta['code'] : editorCodeFromAlertType(alert.alertType);
49
+ const context = typeof meta['context'] === 'object' && meta['context'] !== null ? meta['context'] : {};
50
+ return editorMessageFor(code, context);
51
+ }
52
+ function relTime(iso) {
53
+ const diff = Date.now() - new Date(iso).getTime();
54
+ const s = Math.floor(diff / 1000);
55
+ if (s < 60) {
56
+ return `${s}s ago`;
57
+ }
58
+ if (s < 3600) {
59
+ return `${Math.floor(s / 60)}m ago`;
60
+ }
61
+ if (s < 86_400) {
62
+ return `${Math.floor(s / 3600)}h ago`;
63
+ }
64
+ return `${Math.floor(s / 86_400)}d ago`;
65
+ }
66
+ export const AlertBanner = ({ basePath })=>{
67
+ const [alerts, setAlerts] = useState([]);
68
+ const [dismissingIds, setDismissingIds] = useState(new Set());
69
+ const [showAllGroups, setShowAllGroups] = useState(false);
70
+ useEffect(()=>{
71
+ let cancelled = false;
72
+ let timer = null;
73
+ // Tracks whether the last successful poll returned any alerts.
74
+ // Drives the next-tick cadence (active 30s vs idle 120s).
75
+ let hasActiveAlerts = false;
76
+ async function poll() {
77
+ try {
78
+ const since = new Date(Date.now() - ALERT_LOOKBACK_MS).toISOString();
79
+ const url = `${basePath}/api/translation-alerts` + '?where[dismissed][equals]=false' + `&where[createdAt][greater_than]=${encodeURIComponent(since)}` + `&sort=-createdAt&limit=${ALERT_PAGE_LIMIT}&depth=0`;
80
+ const r = await fetch(url, {
81
+ credentials: 'include'
82
+ });
83
+ if (!r.ok) {
84
+ return;
85
+ }
86
+ const d = await r.json();
87
+ if (!cancelled) {
88
+ const docs = d.docs ?? [];
89
+ setAlerts(docs);
90
+ hasActiveAlerts = docs.length > 0;
91
+ }
92
+ } catch {
93
+ // Best-effort polling — swallow network failures.
94
+ } finally{
95
+ // Visibility-aware + cadence-aware polling (matches the
96
+ // pattern used by `ActiveJobs` and `useBulkTranslateActive`):
97
+ // skip polls while the tab is hidden (the visibilitychange
98
+ // listener below re-fires when the tab returns), and back off
99
+ // to a 2-minute idle cadence when no alerts are showing.
100
+ if (!cancelled && document.visibilityState === 'visible') {
101
+ const nextDelay = hasActiveAlerts ? POLL_INTERVAL_MS : IDLE_POLL_INTERVAL_MS;
102
+ timer = setTimeout(poll, nextDelay);
103
+ }
104
+ }
105
+ }
106
+ function onVisibility() {
107
+ if (document.visibilityState === 'visible' && !cancelled) {
108
+ if (timer) {
109
+ clearTimeout(timer);
110
+ }
111
+ poll();
112
+ }
113
+ }
114
+ document.addEventListener('visibilitychange', onVisibility);
115
+ poll();
116
+ return ()=>{
117
+ cancelled = true;
118
+ if (timer) {
119
+ clearTimeout(timer);
120
+ }
121
+ document.removeEventListener('visibilitychange', onVisibility);
122
+ };
123
+ }, [
124
+ basePath
125
+ ]);
126
+ async function dismiss(id) {
127
+ setDismissingIds((prev)=>new Set(prev).add(id));
128
+ // Optimistic local removal — the next poll will re-confirm.
129
+ setAlerts((prev)=>prev.filter((a)=>a.id !== id));
130
+ try {
131
+ await fetch(`${basePath}/api/translation-alerts/${id}`, {
132
+ method: 'PATCH',
133
+ credentials: 'include',
134
+ headers: {
135
+ 'content-type': 'application/json'
136
+ },
137
+ body: JSON.stringify({
138
+ dismissed: true
139
+ })
140
+ });
141
+ } catch {
142
+ // If the PATCH fails the next poll will surface the alert again.
143
+ } finally{
144
+ setDismissingIds((prev)=>{
145
+ const next = new Set(prev);
146
+ next.delete(id);
147
+ return next;
148
+ });
149
+ }
150
+ }
151
+ /**
152
+ * HUB-1: bulk-dismiss every currently-visible alert in one go.
153
+ * Optimistically clears local state; PATCH calls fan out in parallel.
154
+ * No bulk endpoint exists yet — per-id PATCH is the cheapest path
155
+ * without adding a new endpoint to the plugin surface. We bound by
156
+ * the page limit (30) so a runaway burst doesn't fan out unbounded.
157
+ */ async function dismissAll(ids) {
158
+ if (ids.length === 0) {
159
+ return;
160
+ }
161
+ const idSet = new Set(ids);
162
+ setDismissingIds((prev)=>{
163
+ const next = new Set(prev);
164
+ for (const id of ids){
165
+ next.add(id);
166
+ }
167
+ return next;
168
+ });
169
+ setAlerts((prev)=>prev.filter((a)=>!idSet.has(a.id)));
170
+ await Promise.allSettled(ids.map((id)=>fetch(`${basePath}/api/translation-alerts/${id}`, {
171
+ method: 'PATCH',
172
+ credentials: 'include',
173
+ headers: {
174
+ 'content-type': 'application/json'
175
+ },
176
+ body: JSON.stringify({
177
+ dismissed: true
178
+ })
179
+ })));
180
+ setDismissingIds((prev)=>{
181
+ const next = new Set(prev);
182
+ for (const id of ids){
183
+ next.delete(id);
184
+ }
185
+ return next;
186
+ });
187
+ }
188
+ // Memo the grouping so re-renders driven by `dismissingIds` don't
189
+ // re-shuffle group order. `groupAlerts` is pure so referential
190
+ // stability on `alerts` is enough.
191
+ const groups = useMemo(()=>groupAlerts(alerts), [
192
+ alerts
193
+ ]);
194
+ const split = useMemo(()=>splitVisibleGroups(groups, showAllGroups ? groups.length : MAX_VISIBLE_GROUPS), [
195
+ groups,
196
+ showAllGroups
197
+ ]);
198
+ if (alerts.length === 0) {
199
+ return null;
200
+ }
201
+ const allIds = alerts.map((a)=>a.id);
202
+ const bulkBusy = allIds.every((id)=>dismissingIds.has(id));
203
+ return /*#__PURE__*/ _jsxs("div", {
204
+ style: {
205
+ display: 'flex',
206
+ flexDirection: 'column',
207
+ gap: '0.5rem'
208
+ },
209
+ children: [
210
+ alerts.length > 1 && /*#__PURE__*/ _jsxs("div", {
211
+ style: {
212
+ display: 'flex',
213
+ alignItems: 'center',
214
+ justifyContent: 'space-between',
215
+ gap: '0.75rem',
216
+ padding: '0.4rem 0.75rem',
217
+ background: 'var(--theme-elevation-50)',
218
+ border: '1px solid var(--theme-elevation-150)',
219
+ borderRadius: '4px',
220
+ fontSize: '0.75rem',
221
+ color: 'var(--theme-elevation-700)'
222
+ },
223
+ children: [
224
+ /*#__PURE__*/ _jsxs("span", {
225
+ children: [
226
+ /*#__PURE__*/ _jsx("strong", {
227
+ style: {
228
+ color: 'var(--theme-elevation-900)'
229
+ },
230
+ children: alerts.length
231
+ }),
232
+ " alert",
233
+ alerts.length === 1 ? '' : 's',
234
+ " across",
235
+ ' ',
236
+ /*#__PURE__*/ _jsx("strong", {
237
+ style: {
238
+ color: 'var(--theme-elevation-900)'
239
+ },
240
+ children: groups.length
241
+ }),
242
+ " group",
243
+ groups.length === 1 ? '' : 's',
244
+ split.hiddenGroupCount > 0 && !showAllGroups ? ` · ${split.hiddenAlertCount} hidden` : ''
245
+ ]
246
+ }),
247
+ /*#__PURE__*/ _jsx("button", {
248
+ "data-testid": "translation-alerts-dismiss-all",
249
+ disabled: bulkBusy,
250
+ onClick: ()=>dismissAll(allIds),
251
+ style: {
252
+ padding: '0.25rem 0.6rem',
253
+ background: 'transparent',
254
+ color: 'var(--theme-elevation-900)',
255
+ border: '1px solid var(--theme-elevation-300)',
256
+ borderRadius: '4px',
257
+ fontSize: '0.75rem',
258
+ fontWeight: 600,
259
+ cursor: bulkBusy ? 'wait' : 'pointer'
260
+ },
261
+ title: "Dismiss every alert currently visible",
262
+ type: "button",
263
+ children: "Dismiss all"
264
+ })
265
+ ]
266
+ }),
267
+ split.visible.map((group)=>/*#__PURE__*/ _jsx(AlertGroupBanner, {
268
+ basePath: basePath,
269
+ dismissingIds: dismissingIds,
270
+ group: group,
271
+ onDismiss: dismiss,
272
+ onDismissGroup: (ids)=>void dismissAll(ids)
273
+ }, group.key)),
274
+ split.hiddenGroupCount > 0 && /*#__PURE__*/ _jsx("button", {
275
+ "data-testid": "translation-alerts-show-more",
276
+ onClick: ()=>setShowAllGroups((prev)=>!prev),
277
+ style: {
278
+ alignSelf: 'flex-start',
279
+ padding: '0.3rem 0.6rem',
280
+ background: 'transparent',
281
+ border: '1px dashed var(--theme-elevation-300)',
282
+ borderRadius: '4px',
283
+ color: 'var(--theme-elevation-700)',
284
+ fontSize: '0.75rem',
285
+ cursor: 'pointer'
286
+ },
287
+ type: "button",
288
+ children: showAllGroups ? 'Collapse to top groups' : `+ ${split.hiddenGroupCount} more group${split.hiddenGroupCount === 1 ? '' : 's'} (${split.hiddenAlertCount} alert${split.hiddenAlertCount === 1 ? '' : 's'})`
289
+ })
290
+ ]
291
+ });
292
+ };
293
+ /**
294
+ * Render one group as a single banner. The newest alert in the group
295
+ * provides the title / body / link; the group's `alerts.length`
296
+ * surfaces as a `× N` count badge. Expanding the group reveals the
297
+ * per-alert dismiss controls so an editor can deal with specific docs
298
+ * inside a group.
299
+ */ const AlertGroupBanner = ({ basePath, group, dismissingIds, onDismiss, onDismissGroup })=>{
300
+ const [expanded, setExpanded] = useState(false);
301
+ // Sorting newest-first inside the group so the headline alert is the
302
+ // most recent — matches the "top of stack = newest" convention.
303
+ const sorted = useMemo(()=>[
304
+ ...group.alerts
305
+ ].sort((a, b)=>a.createdAt < b.createdAt ? 1 : -1), [
306
+ group.alerts
307
+ ]);
308
+ const headline = sorted[0];
309
+ const colors = ALERT_COLORS[group.alertType];
310
+ const friendly = readAlertMessage(headline);
311
+ const isMultiple = sorted.length > 1;
312
+ const groupIds = sorted.map((a)=>a.id);
313
+ const allBusy = groupIds.every((id)=>dismissingIds.has(id));
314
+ const docLink = headline.collection && headline.documentId ? docHref(basePath, headline.collection, headline.documentId, group.locale) : null;
315
+ return /*#__PURE__*/ _jsxs("div", {
316
+ style: {
317
+ display: 'flex',
318
+ flexDirection: 'column',
319
+ gap: '0.5rem',
320
+ padding: '0.75rem 1rem',
321
+ background: colors.bg,
322
+ border: `1px solid ${colors.border}`,
323
+ borderRadius: '6px'
324
+ },
325
+ children: [
326
+ /*#__PURE__*/ _jsxs("div", {
327
+ style: {
328
+ display: 'flex',
329
+ gap: '0.75rem',
330
+ alignItems: 'center'
331
+ },
332
+ children: [
333
+ /*#__PURE__*/ _jsxs("div", {
334
+ style: {
335
+ flex: 1,
336
+ minWidth: 0
337
+ },
338
+ children: [
339
+ /*#__PURE__*/ _jsxs("div", {
340
+ style: {
341
+ fontSize: '0.875rem',
342
+ fontWeight: 600,
343
+ color: colors.fg,
344
+ marginBottom: '0.15rem',
345
+ display: 'flex',
346
+ alignItems: 'center',
347
+ gap: '0.4rem',
348
+ flexWrap: 'wrap'
349
+ },
350
+ children: [
351
+ /*#__PURE__*/ _jsx("span", {
352
+ children: friendly.title
353
+ }),
354
+ isMultiple && /*#__PURE__*/ _jsxs("span", {
355
+ style: {
356
+ padding: '0.05rem 0.4rem',
357
+ background: colors.border,
358
+ color: 'var(--theme-elevation-0)',
359
+ borderRadius: '999px',
360
+ fontSize: '0.7rem',
361
+ fontWeight: 600
362
+ },
363
+ title: `${sorted.length} alerts in the last ${group.bucket}`,
364
+ children: [
365
+ "× ",
366
+ sorted.length
367
+ ]
368
+ }),
369
+ /*#__PURE__*/ _jsxs("span", {
370
+ style: {
371
+ marginLeft: '0.25rem',
372
+ fontWeight: 400,
373
+ fontSize: '0.75rem',
374
+ color: 'var(--theme-elevation-700)'
375
+ },
376
+ children: [
377
+ "· ",
378
+ relTime(headline.createdAt),
379
+ isMultiple ? ` · ${group.bucket}` : ''
380
+ ]
381
+ })
382
+ ]
383
+ }),
384
+ /*#__PURE__*/ _jsxs("div", {
385
+ style: {
386
+ fontSize: '0.8rem',
387
+ color: 'var(--theme-elevation-900)',
388
+ lineHeight: 1.45
389
+ },
390
+ title: headline.message,
391
+ children: [
392
+ friendly.body,
393
+ headline.collection && /*#__PURE__*/ _jsxs(_Fragment, {
394
+ children: [
395
+ ' — ',
396
+ docLink && !isMultiple ? /*#__PURE__*/ _jsx("a", {
397
+ href: docLink,
398
+ rel: "noopener noreferrer",
399
+ style: {
400
+ color: 'var(--theme-success-500)',
401
+ textDecoration: 'none'
402
+ },
403
+ target: "_blank",
404
+ children: /*#__PURE__*/ _jsxs("code", {
405
+ children: [
406
+ headline.collection,
407
+ headline.documentId ? ` #${headline.documentId}` : ''
408
+ ]
409
+ })
410
+ }) : /*#__PURE__*/ _jsxs("code", {
411
+ style: {
412
+ color: 'var(--theme-elevation-700)'
413
+ },
414
+ children: [
415
+ headline.collection,
416
+ isMultiple ? ` · ${sorted.length} docs` : ''
417
+ ]
418
+ })
419
+ ]
420
+ })
421
+ ]
422
+ }),
423
+ friendly.action && /*#__PURE__*/ _jsx("div", {
424
+ style: {
425
+ fontSize: '0.75rem',
426
+ color: 'var(--theme-elevation-700)',
427
+ fontStyle: 'italic',
428
+ marginTop: '0.25rem'
429
+ },
430
+ children: friendly.action
431
+ })
432
+ ]
433
+ }),
434
+ !isMultiple && docLink && /*#__PURE__*/ _jsx("a", {
435
+ href: docLink,
436
+ rel: "noopener noreferrer",
437
+ style: {
438
+ padding: '0.25rem 0.6rem',
439
+ background: 'transparent',
440
+ color: colors.fg,
441
+ border: `1px solid ${colors.border}`,
442
+ borderRadius: '4px',
443
+ fontSize: '0.75rem',
444
+ textDecoration: 'none',
445
+ whiteSpace: 'nowrap',
446
+ display: 'inline-block'
447
+ },
448
+ target: "_blank",
449
+ title: "Open the document this alert refers to",
450
+ children: "View document →"
451
+ }),
452
+ isMultiple && /*#__PURE__*/ _jsx("button", {
453
+ "aria-expanded": expanded,
454
+ "data-testid": "translation-alert-group-expand",
455
+ onClick: ()=>setExpanded((prev)=>!prev),
456
+ style: {
457
+ padding: '0.25rem 0.6rem',
458
+ background: 'transparent',
459
+ color: colors.fg,
460
+ border: `1px solid ${colors.border}`,
461
+ borderRadius: '4px',
462
+ fontSize: '0.75rem',
463
+ cursor: 'pointer',
464
+ whiteSpace: 'nowrap'
465
+ },
466
+ type: "button",
467
+ children: expanded ? 'Hide details' : `Show ${sorted.length}`
468
+ }),
469
+ /*#__PURE__*/ _jsx("button", {
470
+ disabled: isMultiple ? allBusy : dismissingIds.has(headline.id),
471
+ onClick: ()=>isMultiple ? onDismissGroup(groupIds) : onDismiss(headline.id),
472
+ style: {
473
+ padding: '0.25rem 0.6rem',
474
+ background: 'transparent',
475
+ color: colors.fg,
476
+ border: `1px solid ${colors.border}`,
477
+ borderRadius: '4px',
478
+ fontSize: '0.75rem',
479
+ cursor: 'pointer',
480
+ whiteSpace: 'nowrap'
481
+ },
482
+ title: isMultiple ? 'Dismiss every alert in this group' : 'Dismiss',
483
+ type: "button",
484
+ children: isMultiple ? 'Dismiss group' : 'Dismiss'
485
+ })
486
+ ]
487
+ }),
488
+ isMultiple && expanded && /*#__PURE__*/ _jsx("ul", {
489
+ style: {
490
+ margin: 0,
491
+ padding: '0.5rem 0 0.25rem',
492
+ listStyle: 'none',
493
+ display: 'flex',
494
+ flexDirection: 'column',
495
+ gap: '0.3rem',
496
+ borderTop: `1px dashed ${colors.border}`
497
+ },
498
+ children: sorted.map((alert)=>{
499
+ const alertDoc = alert.collection && alert.documentId ? docHref(basePath, alert.collection, alert.documentId, group.locale) : null;
500
+ const busy = dismissingIds.has(alert.id);
501
+ return /*#__PURE__*/ _jsxs("li", {
502
+ style: {
503
+ display: 'flex',
504
+ alignItems: 'center',
505
+ justifyContent: 'space-between',
506
+ gap: '0.5rem',
507
+ fontSize: '0.75rem',
508
+ color: 'var(--theme-elevation-900)'
509
+ },
510
+ children: [
511
+ /*#__PURE__*/ _jsxs("span", {
512
+ style: {
513
+ minWidth: 0,
514
+ overflow: 'hidden',
515
+ textOverflow: 'ellipsis'
516
+ },
517
+ children: [
518
+ alertDoc ? /*#__PURE__*/ _jsx("a", {
519
+ href: alertDoc,
520
+ rel: "noopener noreferrer",
521
+ style: {
522
+ color: 'var(--theme-success-500)',
523
+ textDecoration: 'none'
524
+ },
525
+ target: "_blank",
526
+ children: /*#__PURE__*/ _jsxs("code", {
527
+ children: [
528
+ alert.collection,
529
+ alert.documentId ? ` #${alert.documentId}` : ''
530
+ ]
531
+ })
532
+ }) : /*#__PURE__*/ _jsx("code", {
533
+ style: {
534
+ color: 'var(--theme-elevation-700)'
535
+ },
536
+ children: alert.collection ?? 'unknown'
537
+ }),
538
+ /*#__PURE__*/ _jsx("span", {
539
+ style: {
540
+ marginLeft: '0.5rem',
541
+ color: 'var(--theme-elevation-700)'
542
+ },
543
+ children: relTime(alert.createdAt)
544
+ })
545
+ ]
546
+ }),
547
+ /*#__PURE__*/ _jsx("button", {
548
+ disabled: busy,
549
+ onClick: ()=>onDismiss(alert.id),
550
+ style: {
551
+ padding: '0.1rem 0.4rem',
552
+ background: 'transparent',
553
+ color: colors.fg,
554
+ border: `1px solid ${colors.border}`,
555
+ borderRadius: '4px',
556
+ fontSize: '0.7rem',
557
+ cursor: busy ? 'wait' : 'pointer'
558
+ },
559
+ type: "button",
560
+ children: "Dismiss"
561
+ })
562
+ ]
563
+ }, alert.id);
564
+ })
565
+ })
566
+ ]
567
+ });
568
+ };
@@ -0,0 +1,6 @@
1
+ import type React from 'react';
2
+ interface Props {
3
+ basePath: string;
4
+ }
5
+ export declare const AuditPanel: React.FC<Props>;
6
+ export default AuditPanel;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Pure helpers extracted from `AuditPanel.tsx` so the reconciliation
3
+ * logic flagged in v1.2.6 BugIndex NEW-1 / NEW-2 can be unit-tested
4
+ * without dragging React or `fetch` into the test environment.
5
+ *
6
+ * - `isRealModelId` — decides whether a `model` string on a
7
+ * usage row counts as an actual model
8
+ * (provider/model slug) vs a provider-only
9
+ * fallback like `'openrouter'` that should
10
+ * collapse into "Failed before model
11
+ * selection". See NEW-2.
12
+ * - `summarizeFailedCounts` — reconciles the doc-level "failed" KPI
13
+ * with the per-locale-row "failed" total
14
+ * so the editor sees both numbers labeled.
15
+ * See NEW-1.
16
+ */
17
+ export type AuditUsageRowLike = {
18
+ status: 'succeeded' | 'failed';
19
+ model?: string | null;
20
+ targetLocales?: Array<{
21
+ locale: string;
22
+ status?: 'succeeded' | 'failed' | null;
23
+ }> | null;
24
+ };
25
+ export declare function isRealModelId(model: string | null | undefined): boolean;
26
+ /**
27
+ * Two distinct units of "failed" need to be surfaced separately to
28
+ * reconcile the AuditPanel KPI card with the Target locales table:
29
+ *
30
+ * - `docFailed` — usage rows whose `status === 'failed'`. One row
31
+ * per (collection, doc, run); the unit the top KPI
32
+ * reports.
33
+ * - `localeFailed` — entries in `targetLocales[]` whose status is
34
+ * `'failed'`. A run targeting 2 locales contributes
35
+ * 2 entries; the unit the per-locale table reports.
36
+ *
37
+ * The two are NOT expected to match — a doc that partially fails counts
38
+ * once at doc level but multiple times at locale level.
39
+ */
40
+ export declare function summarizeFailedCounts(rows: AuditUsageRowLike[]): {
41
+ docFailed: number;
42
+ localeFailed: number;
43
+ localeRows: number;
44
+ };