@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.
- package/LICENSE +21 -0
- package/README.md +714 -0
- package/dist/alerts-collection.d.ts +21 -0
- package/dist/alerts-collection.js +159 -0
- package/dist/api.d.ts +4 -0
- package/dist/api.js +918 -0
- package/dist/bulk-translate-batches-collection.d.ts +29 -0
- package/dist/bulk-translate-batches-collection.js +404 -0
- package/dist/bulk-translate-units-collection.d.ts +35 -0
- package/dist/bulk-translate-units-collection.js +310 -0
- package/dist/client/estimated-cost-cell.d.ts +6 -0
- package/dist/client/estimated-cost-cell.js +12 -0
- package/dist/client/excluded-fields-field.d.ts +45 -0
- package/dist/client/excluded-fields-field.js +553 -0
- package/dist/client/field-translate-button.d.ts +6 -0
- package/dist/client/field-translate-button.js +199 -0
- package/dist/client/index.d.ts +6 -0
- package/dist/client/index.js +6 -0
- package/dist/client/lib/use-global-kill-switches.d.ts +20 -0
- package/dist/client/lib/use-global-kill-switches.js +58 -0
- package/dist/client/translate-button.d.ts +2 -0
- package/dist/client/translate-button.js +228 -0
- package/dist/client/translate-modal.d.ts +16 -0
- package/dist/client/translate-modal.js +549 -0
- package/dist/client/translation-progress.d.ts +10 -0
- package/dist/client/translation-progress.js +297 -0
- package/dist/components/TranslationNavGroup.d.ts +45 -0
- package/dist/components/TranslationNavGroup.js +104 -0
- package/dist/defaults.d.ts +11 -0
- package/dist/defaults.js +16 -0
- package/dist/endpoints/client-config.d.ts +44 -0
- package/dist/endpoints/client-config.js +145 -0
- package/dist/endpoints/estimate.d.ts +5 -0
- package/dist/endpoints/estimate.js +237 -0
- package/dist/endpoints/progress.d.ts +2 -0
- package/dist/endpoints/progress.js +314 -0
- package/dist/endpoints/translate.d.ts +11 -0
- package/dist/endpoints/translate.js +376 -0
- package/dist/endpoints/translation-hub/_helpers.d.ts +140 -0
- package/dist/endpoints/translation-hub/_helpers.js +297 -0
- package/dist/endpoints/translation-hub/active.d.ts +21 -0
- package/dist/endpoints/translation-hub/active.js +220 -0
- package/dist/endpoints/translation-hub/cancel.d.ts +22 -0
- package/dist/endpoints/translation-hub/cancel.js +233 -0
- package/dist/endpoints/translation-hub/enqueue.d.ts +70 -0
- package/dist/endpoints/translation-hub/enqueue.js +529 -0
- package/dist/endpoints/translation-hub/failures.d.ts +12 -0
- package/dist/endpoints/translation-hub/failures.js +67 -0
- package/dist/endpoints/translation-hub/force-reset.d.ts +20 -0
- package/dist/endpoints/translation-hub/force-reset.js +144 -0
- package/dist/endpoints/translation-hub/index.d.ts +21 -0
- package/dist/endpoints/translation-hub/index.js +20 -0
- package/dist/endpoints/translation-hub/list.d.ts +40 -0
- package/dist/endpoints/translation-hub/list.js +182 -0
- package/dist/endpoints/translation-hub/preflight.d.ts +19 -0
- package/dist/endpoints/translation-hub/preflight.js +141 -0
- package/dist/endpoints/translation-hub/retry-failed.d.ts +38 -0
- package/dist/endpoints/translation-hub/retry-failed.js +235 -0
- package/dist/endpoints/translation-hub/revert.d.ts +88 -0
- package/dist/endpoints/translation-hub/revert.js +405 -0
- package/dist/endpoints/translation-hub/status.d.ts +45 -0
- package/dist/endpoints/translation-hub/status.js +391 -0
- package/dist/endpoints/translation-hub/usage-summary.d.ts +114 -0
- package/dist/endpoints/translation-hub/usage-summary.js +481 -0
- package/dist/exports/client.d.ts +6 -0
- package/dist/exports/client.js +6 -0
- package/dist/exports/components.d.ts +6 -0
- package/dist/exports/components.js +5 -0
- package/dist/exports/index.d.ts +8 -0
- package/dist/exports/index.js +7 -0
- package/dist/exports/providers.d.ts +9 -0
- package/dist/exports/providers.js +5 -0
- package/dist/exports/views-client.d.ts +23 -0
- package/dist/exports/views-client.js +22 -0
- package/dist/exports/views.d.ts +30 -0
- package/dist/exports/views.js +29 -0
- package/dist/hooks/after-change-global.d.ts +4 -0
- package/dist/hooks/after-change-global.js +109 -0
- package/dist/hooks/after-change.d.ts +16 -0
- package/dist/hooks/after-change.js +205 -0
- package/dist/hooks/after-delete.d.ts +30 -0
- package/dist/hooks/after-delete.js +95 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/jobs-collection.d.ts +17 -0
- package/dist/jobs-collection.js +139 -0
- package/dist/lexical/classifier.d.ts +3 -0
- package/dist/lexical/classifier.js +108 -0
- package/dist/lexical/deserializer.d.ts +4 -0
- package/dist/lexical/deserializer.js +263 -0
- package/dist/lexical/placeholder-integrity.d.ts +6 -0
- package/dist/lexical/placeholder-integrity.js +21 -0
- package/dist/lexical/placeholders.d.ts +21 -0
- package/dist/lexical/placeholders.js +117 -0
- package/dist/lexical/serializer.d.ts +21 -0
- package/dist/lexical/serializer.js +233 -0
- package/dist/lexical/types.d.ts +32 -0
- package/dist/lexical/types.js +1 -0
- package/dist/lib/auth-diagnostics.d.ts +14 -0
- package/dist/lib/auth-diagnostics.js +19 -0
- package/dist/lib/batch-counts.d.ts +58 -0
- package/dist/lib/batch-counts.js +105 -0
- package/dist/lib/bulk-translate-migrations.d.ts +92 -0
- package/dist/lib/bulk-translate-migrations.js +153 -0
- package/dist/lib/coalescing-queue.d.ts +38 -0
- package/dist/lib/coalescing-queue.js +69 -0
- package/dist/lib/content-extractor.d.ts +16 -0
- package/dist/lib/content-extractor.js +410 -0
- package/dist/lib/content-hash.d.ts +1 -0
- package/dist/lib/content-hash.js +19 -0
- package/dist/lib/content-patcher.d.ts +15 -0
- package/dist/lib/content-patcher.js +293 -0
- package/dist/lib/cost-guards.d.ts +2 -0
- package/dist/lib/cost-guards.js +18 -0
- package/dist/lib/daily-spend-cap.d.ts +58 -0
- package/dist/lib/daily-spend-cap.js +233 -0
- package/dist/lib/effective-locales.d.ts +181 -0
- package/dist/lib/effective-locales.js +302 -0
- package/dist/lib/error-messages.d.ts +245 -0
- package/dist/lib/error-messages.js +626 -0
- package/dist/lib/events.d.ts +39 -0
- package/dist/lib/events.js +146 -0
- package/dist/lib/exclude-fields.d.ts +3 -0
- package/dist/lib/exclude-fields.js +64 -0
- package/dist/lib/field-breadcrumb.d.ts +31 -0
- package/dist/lib/field-breadcrumb.js +227 -0
- package/dist/lib/field-diff.d.ts +1 -0
- package/dist/lib/field-diff.js +25 -0
- package/dist/lib/field-empty.d.ts +2 -0
- package/dist/lib/field-empty.js +68 -0
- package/dist/lib/field-resolver.d.ts +3 -0
- package/dist/lib/field-resolver.js +164 -0
- package/dist/lib/group-soft-skips.d.ts +39 -0
- package/dist/lib/group-soft-skips.js +45 -0
- package/dist/lib/locale-merge.d.ts +44 -0
- package/dist/lib/locale-merge.js +357 -0
- package/dist/lib/locale-row-check.d.ts +30 -0
- package/dist/lib/locale-row-check.js +64 -0
- package/dist/lib/logger.d.ts +74 -0
- package/dist/lib/logger.js +97 -0
- package/dist/lib/manual-edit-guard.d.ts +128 -0
- package/dist/lib/manual-edit-guard.js +393 -0
- package/dist/lib/output-validation.d.ts +48 -0
- package/dist/lib/output-validation.js +148 -0
- package/dist/lib/payload-read.d.ts +16 -0
- package/dist/lib/payload-read.js +51 -0
- package/dist/lib/per-doc-claim.d.ts +90 -0
- package/dist/lib/per-doc-claim.js +140 -0
- package/dist/lib/per-doc-lock.d.ts +94 -0
- package/dist/lib/per-doc-lock.js +119 -0
- package/dist/lib/persist-usage.d.ts +91 -0
- package/dist/lib/persist-usage.js +116 -0
- package/dist/lib/progress-store.d.ts +103 -0
- package/dist/lib/progress-store.js +314 -0
- package/dist/lib/rate-limiter.d.ts +3 -0
- package/dist/lib/rate-limiter.js +53 -0
- package/dist/lib/snapshot-select.d.ts +43 -0
- package/dist/lib/snapshot-select.js +108 -0
- package/dist/lib/translate-prompt.d.ts +31 -0
- package/dist/lib/translate-prompt.js +66 -0
- package/dist/lib/translation-token-bucket.d.ts +57 -0
- package/dist/lib/translation-token-bucket.js +365 -0
- package/dist/lib/truncate-source-value.d.ts +1 -0
- package/dist/lib/truncate-source-value.js +27 -0
- package/dist/manual-edit-collection.d.ts +22 -0
- package/dist/manual-edit-collection.js +124 -0
- package/dist/plugin.d.ts +3 -0
- package/dist/plugin.js +934 -0
- package/dist/providers/ai-sdk-adapter.d.ts +35 -0
- package/dist/providers/ai-sdk-adapter.js +100 -0
- package/dist/providers/anthropic.d.ts +31 -0
- package/dist/providers/anthropic.js +66 -0
- package/dist/providers/custom.d.ts +36 -0
- package/dist/providers/custom.js +24 -0
- package/dist/providers/gemini.d.ts +20 -0
- package/dist/providers/gemini.js +48 -0
- package/dist/providers/mock.d.ts +2 -0
- package/dist/providers/mock.js +29 -0
- package/dist/providers/openai.d.ts +28 -0
- package/dist/providers/openai.js +69 -0
- package/dist/settings-global.d.ts +74 -0
- package/dist/settings-global.js +216 -0
- package/dist/tasks/bulk-translate-coordinator.d.ts +115 -0
- package/dist/tasks/bulk-translate-coordinator.js +708 -0
- package/dist/tasks/bulk-translate-doc-task.d.ts +142 -0
- package/dist/tasks/bulk-translate-doc-task.js +1000 -0
- package/dist/tasks/bulk-translate-janitor.d.ts +87 -0
- package/dist/tasks/bulk-translate-janitor.js +311 -0
- package/dist/tasks/translate-job-task.d.ts +51 -0
- package/dist/tasks/translate-job-task.js +154 -0
- package/dist/translate.d.ts +113 -0
- package/dist/translate.js +911 -0
- package/dist/translation-daily-spend-collection.d.ts +24 -0
- package/dist/translation-daily-spend-collection.js +133 -0
- package/dist/translation-rate-limits-collection.d.ts +30 -0
- package/dist/translation-rate-limits-collection.js +144 -0
- package/dist/types.d.ts +672 -0
- package/dist/types.js +1 -0
- package/dist/usage-collection.d.ts +14 -0
- package/dist/usage-collection.js +377 -0
- package/dist/views/BulkRunsHub/BatchRow.d.ts +32 -0
- package/dist/views/BulkRunsHub/BatchRow.js +1222 -0
- package/dist/views/BulkRunsHub/BucketRow.d.ts +62 -0
- package/dist/views/BulkRunsHub/BucketRow.js +982 -0
- package/dist/views/BulkRunsHub/BulkRunsHub.client.d.ts +18 -0
- package/dist/views/BulkRunsHub/BulkRunsHub.client.js +331 -0
- package/dist/views/BulkRunsHub/EmptyState.d.ts +6 -0
- package/dist/views/BulkRunsHub/EmptyState.js +64 -0
- package/dist/views/BulkRunsHub/FilterBar.d.ts +16 -0
- package/dist/views/BulkRunsHub/FilterBar.js +284 -0
- package/dist/views/BulkRunsHub/InFlightBanner.d.ts +14 -0
- package/dist/views/BulkRunsHub/InFlightBanner.js +59 -0
- package/dist/views/BulkRunsHub/StatusBadge.d.ts +64 -0
- package/dist/views/BulkRunsHub/StatusBadge.js +248 -0
- package/dist/views/BulkRunsHub/SummaryStrip.d.ts +22 -0
- package/dist/views/BulkRunsHub/SummaryStrip.js +249 -0
- package/dist/views/BulkRunsHub/bucket-grouping.d.ts +200 -0
- package/dist/views/BulkRunsHub/bucket-grouping.js +344 -0
- package/dist/views/BulkRunsHub/bucketFailureSummary.d.ts +9 -0
- package/dist/views/BulkRunsHub/bucketFailureSummary.js +36 -0
- package/dist/views/BulkRunsHub/dedupedStatusFetch.d.ts +5 -0
- package/dist/views/BulkRunsHub/dedupedStatusFetch.js +45 -0
- package/dist/views/BulkRunsHub/index.d.ts +17 -0
- package/dist/views/BulkRunsHub/index.js +80 -0
- package/dist/views/BulkRunsHub/urlFilters.d.ts +14 -0
- package/dist/views/BulkRunsHub/urlFilters.js +50 -0
- package/dist/views/BulkRunsHub/useBulkRunsList.d.ts +26 -0
- package/dist/views/BulkRunsHub/useBulkRunsList.js +204 -0
- package/dist/views/BulkRunsHub/useUrlFilters.d.ts +10 -0
- package/dist/views/BulkRunsHub/useUrlFilters.js +88 -0
- package/dist/views/TranslationHub/ActiveJobs.d.ts +6 -0
- package/dist/views/TranslationHub/ActiveJobs.js +320 -0
- package/dist/views/TranslationHub/AdvancedPanel.d.ts +17 -0
- package/dist/views/TranslationHub/AdvancedPanel.js +996 -0
- package/dist/views/TranslationHub/AlertBanner.d.ts +6 -0
- package/dist/views/TranslationHub/AlertBanner.js +568 -0
- package/dist/views/TranslationHub/AuditPanel.d.ts +6 -0
- package/dist/views/TranslationHub/AuditPanel.helpers.d.ts +44 -0
- package/dist/views/TranslationHub/AuditPanel.helpers.js +71 -0
- package/dist/views/TranslationHub/AuditPanel.js +1367 -0
- package/dist/views/TranslationHub/BulkTranslate.types.d.ts +242 -0
- package/dist/views/TranslationHub/BulkTranslate.types.js +36 -0
- package/dist/views/TranslationHub/BulkTranslateFailureDrawer.d.ts +19 -0
- package/dist/views/TranslationHub/BulkTranslateFailureDrawer.js +332 -0
- package/dist/views/TranslationHub/BulkTranslateMonitor.d.ts +28 -0
- package/dist/views/TranslationHub/BulkTranslateMonitor.js +305 -0
- package/dist/views/TranslationHub/BulkTranslateNarrowViewportBanner.d.ts +3 -0
- package/dist/views/TranslationHub/BulkTranslateNarrowViewportBanner.js +42 -0
- package/dist/views/TranslationHub/BulkTranslatePostEnqueueTransition.d.ts +26 -0
- package/dist/views/TranslationHub/BulkTranslatePostEnqueueTransition.js +95 -0
- package/dist/views/TranslationHub/BulkTranslatePreflightModal.d.ts +22 -0
- package/dist/views/TranslationHub/BulkTranslatePreflightModal.js +879 -0
- package/dist/views/TranslationHub/BulkTranslateTerminalCard.d.ts +29 -0
- package/dist/views/TranslationHub/BulkTranslateTerminalCard.js +445 -0
- package/dist/views/TranslationHub/BulkTranslateTrigger.d.ts +66 -0
- package/dist/views/TranslationHub/BulkTranslateTrigger.js +161 -0
- package/dist/views/TranslationHub/EditorRecentRunsPanel.d.ts +33 -0
- package/dist/views/TranslationHub/EditorRecentRunsPanel.js +290 -0
- package/dist/views/TranslationHub/Hub.client.d.ts +74 -0
- package/dist/views/TranslationHub/Hub.client.js +357 -0
- package/dist/views/TranslationHub/ModelCombobox.d.ts +14 -0
- package/dist/views/TranslationHub/ModelCombobox.js +415 -0
- package/dist/views/TranslationHub/PerCollectionConfig.d.ts +10 -0
- package/dist/views/TranslationHub/PerCollectionConfig.helpers.d.ts +16 -0
- package/dist/views/TranslationHub/PerCollectionConfig.helpers.js +19 -0
- package/dist/views/TranslationHub/PerCollectionConfig.js +759 -0
- package/dist/views/TranslationHub/SettingsRail.d.ts +11 -0
- package/dist/views/TranslationHub/SettingsRail.js +382 -0
- package/dist/views/TranslationHub/StatusStrip.d.ts +6 -0
- package/dist/views/TranslationHub/StatusStrip.js +451 -0
- package/dist/views/TranslationHub/UsageTable.d.ts +6 -0
- package/dist/views/TranslationHub/UsageTable.helpers.d.ts +69 -0
- package/dist/views/TranslationHub/UsageTable.helpers.js +49 -0
- package/dist/views/TranslationHub/UsageTable.js +1240 -0
- package/dist/views/TranslationHub/alertGrouping.d.ts +70 -0
- package/dist/views/TranslationHub/alertGrouping.js +99 -0
- package/dist/views/TranslationHub/index.d.ts +20 -0
- package/dist/views/TranslationHub/index.js +109 -0
- package/dist/views/TranslationHub/tabNavigation.d.ts +53 -0
- package/dist/views/TranslationHub/tabNavigation.js +74 -0
- package/dist/views/TranslationHub/terminalBannerVisibility.d.ts +33 -0
- package/dist/views/TranslationHub/terminalBannerVisibility.js +124 -0
- package/dist/views/TranslationHub/useBulkTranslateActive.d.ts +49 -0
- package/dist/views/TranslationHub/useBulkTranslateActive.js +251 -0
- package/dist/views/TranslationHub/useFocusTrap.d.ts +6 -0
- package/dist/views/TranslationHub/useFocusTrap.js +81 -0
- package/dist/views/TranslationHub/useTranslationHubUsageSummary.d.ts +77 -0
- package/dist/views/TranslationHub/useTranslationHubUsageSummary.js +267 -0
- package/dist/views/shared/EditorError.d.ts +97 -0
- package/dist/views/shared/EditorError.js +205 -0
- package/dist/views/shared/ModelCell.d.ts +18 -0
- package/dist/views/shared/ModelCell.js +31 -0
- package/dist/views/shared/docHref.d.ts +16 -0
- package/dist/views/shared/docHref.js +26 -0
- package/dist/views/shared/fetch-error-body.d.ts +25 -0
- package/dist/views/shared/fetch-error-body.js +42 -0
- package/dist/views/shared/filterPillStyle.d.ts +35 -0
- package/dist/views/shared/filterPillStyle.js +40 -0
- package/dist/views/shared/format.d.ts +75 -0
- package/dist/views/shared/format.js +131 -0
- package/package.json +141 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { describeAuthRejection } from '../../lib/auth-diagnostics.js';
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// Auth helpers
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
/**
|
|
6
|
+
* Pre-1.2.8: every translation-hub endpoint was admin-only (Decision
|
|
7
|
+
* #31). v1.2.8 opens trigger + read endpoints to editor-role users so
|
|
8
|
+
* content editors can run bulk translation without needing an admin
|
|
9
|
+
* to fire it for them. Admin-only access is preserved for
|
|
10
|
+
* configuration / cost-limit / force-reset surfaces; per-batch
|
|
11
|
+
* mutations (cancel / retry / revert) are gated by `ownsBatch` so
|
|
12
|
+
* one editor cannot disrupt another's run.
|
|
13
|
+
*
|
|
14
|
+
* Defaults to checking for the literal `'admin'` role. Consumers using
|
|
15
|
+
* a different role naming convention should wrap this in their plugin
|
|
16
|
+
* config's `bulk.requireTotp`/audit-role surfaces; the surface here
|
|
17
|
+
* intentionally stays narrow.
|
|
18
|
+
*/ export function isAdminUser(user) {
|
|
19
|
+
if (!user) return false;
|
|
20
|
+
const roles = user?.roles;
|
|
21
|
+
if (!Array.isArray(roles)) return false;
|
|
22
|
+
return roles.includes('admin');
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Returns `true` for admin OR editor roles. Used by the editor-open
|
|
26
|
+
* endpoints (enqueue / active / status / preflight / list / failures)
|
|
27
|
+
* — the per-batch mutation endpoints layer `ownsBatch` on top of this.
|
|
28
|
+
*
|
|
29
|
+
* `editor` is the conventional role name for content editors across
|
|
30
|
+
* the wild-payload-cms-style consumers; consumers using a different
|
|
31
|
+
* role naming convention should add the synonym to the membership
|
|
32
|
+
* list rather than re-implementing the check.
|
|
33
|
+
*/ export function isEditorOrAdmin(user) {
|
|
34
|
+
if (!user) return false;
|
|
35
|
+
const roles = user?.roles;
|
|
36
|
+
if (!Array.isArray(roles)) return false;
|
|
37
|
+
return roles.includes('admin') || roles.includes('editor');
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Ownership check for per-batch mutations (cancel / retry-failed /
|
|
41
|
+
* revert). Admins always pass — they own the world. Editors must be
|
|
42
|
+
* the user who triggered the batch (matched on `triggeredByUserId`).
|
|
43
|
+
*
|
|
44
|
+
* The batch record may carry `triggeredByUserId` as either `string`
|
|
45
|
+
* (the canonical persisted form — see `enqueue.ts:266`) or `number`
|
|
46
|
+
* depending on the auth collection's id type. Compare stringified to
|
|
47
|
+
* sidestep that.
|
|
48
|
+
*/ export function ownsBatch(user, batch) {
|
|
49
|
+
if (!user || !batch) return false;
|
|
50
|
+
if (isAdminUser(user)) return true;
|
|
51
|
+
const userId = user?.id;
|
|
52
|
+
if (userId == null || batch.triggeredByUserId == null) return false;
|
|
53
|
+
return String(userId) === String(batch.triggeredByUserId);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Structured 403 for "you can do this kind of action, but not on
|
|
57
|
+
* THIS batch (you didn't trigger it)". Distinct from the 401
|
|
58
|
+
* `unauthorizedResponse` (cookie / CSRF failure) and from the 403
|
|
59
|
+
* we'd return for an editor hitting an admin-only endpoint — the
|
|
60
|
+
* `code` lets the UI render the right copy.
|
|
61
|
+
*/ export function forbiddenOwnershipResponse() {
|
|
62
|
+
return Response.json({
|
|
63
|
+
error: {
|
|
64
|
+
code: 'forbidden_ownership',
|
|
65
|
+
message: 'You can only modify bulk runs you started. Ask the editor who triggered this run, or contact an admin.'
|
|
66
|
+
}
|
|
67
|
+
}, {
|
|
68
|
+
status: 403
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Structured error JSON used across the translation-hub endpoints.
|
|
73
|
+
* Mirrors the shape an `api-designer` would draft for a public API —
|
|
74
|
+
* `code` is the stable machine-readable identifier (UI surfaces
|
|
75
|
+
* key off it), `message` is human-readable, `fields` carries per-
|
|
76
|
+
* field validation hints when relevant.
|
|
77
|
+
*/ export function errorResponse(code, message, status, fields) {
|
|
78
|
+
const body = {
|
|
79
|
+
error: fields ? {
|
|
80
|
+
code,
|
|
81
|
+
message,
|
|
82
|
+
fields
|
|
83
|
+
} : {
|
|
84
|
+
code,
|
|
85
|
+
message
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
return Response.json(body, {
|
|
89
|
+
status
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Structured 401 with a `diagnostic` field naming why auth was rejected —
|
|
94
|
+
* almost always a CSRF allowlist mismatch when the page was opened via the
|
|
95
|
+
* `Network:` URL Next prints in dev. Without this, the response is a
|
|
96
|
+
* silent "Unauthorized" and the next investigator wastes 30 min on JWTs.
|
|
97
|
+
*/ export function unauthorizedResponse(req) {
|
|
98
|
+
const diagnostic = describeAuthRejection(req);
|
|
99
|
+
return Response.json({
|
|
100
|
+
error: {
|
|
101
|
+
code: 'unauthorized',
|
|
102
|
+
message: 'Authentication required.',
|
|
103
|
+
diagnostic
|
|
104
|
+
}
|
|
105
|
+
}, {
|
|
106
|
+
status: 401
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
export async function verifyTotpCode(payload, userId, totpCode, options) {
|
|
110
|
+
if (!options.required) {
|
|
111
|
+
return {
|
|
112
|
+
ok: true
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (!totpCode || typeof totpCode !== 'string' || totpCode.length === 0) {
|
|
116
|
+
return {
|
|
117
|
+
ok: false,
|
|
118
|
+
code: 'missing',
|
|
119
|
+
message: 'An authentication code is required. Enter the 6-digit code from your authenticator app.'
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
// Lazy import — totp plugin may not be installed in the consumer's
|
|
123
|
+
// app. If the dynamic import fails (or the module shape changes),
|
|
124
|
+
// we treat it as plugin_unavailable.
|
|
125
|
+
let verifyFn = ()=>({
|
|
126
|
+
valid: false
|
|
127
|
+
});
|
|
128
|
+
let decryptFn = (s)=>s;
|
|
129
|
+
try {
|
|
130
|
+
// Use a string variable to make the import path opaque to the
|
|
131
|
+
// bundler — the plugin must remain optional at install time.
|
|
132
|
+
const totpCrypto = '../../../../totp/crypto/totp';
|
|
133
|
+
const cryptoMod = await import(totpCrypto);
|
|
134
|
+
const helpers = '../../../../totp/utilities/helpers';
|
|
135
|
+
const helpersMod = await import(helpers);
|
|
136
|
+
if (typeof cryptoMod.verifyTOTPToken !== 'function' || typeof helpersMod.decryptSecret !== 'function' || typeof helpersMod.isTOTPEncryptionConfigured !== 'function' || !helpersMod.isTOTPEncryptionConfigured()) {
|
|
137
|
+
throw new Error('totp_unavailable');
|
|
138
|
+
}
|
|
139
|
+
verifyFn = cryptoMod.verifyTOTPToken;
|
|
140
|
+
decryptFn = helpersMod.decryptSecret;
|
|
141
|
+
} catch {
|
|
142
|
+
payload.logger?.warn?.('[ai-translate] bulk-translate: TOTP plugin not detected; skipping TOTP check (requireTotp was true).');
|
|
143
|
+
return options.allowSkipWhenUnavailable ? {
|
|
144
|
+
ok: true
|
|
145
|
+
} : {
|
|
146
|
+
ok: false,
|
|
147
|
+
code: 'plugin_unavailable',
|
|
148
|
+
message: "Two-factor authentication is required but isn't available in this environment. Contact engineering."
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
// Look up the user's TOTP secret. The auth collection slug varies
|
|
152
|
+
// per consumer — read it from payload.config (Payload exposes
|
|
153
|
+
// `config.admin.user` as the auth collection slug).
|
|
154
|
+
const authSlug = payload.config.admin?.user ?? 'users';
|
|
155
|
+
let user;
|
|
156
|
+
try {
|
|
157
|
+
// biome-ignore lint/suspicious/noExplicitAny: dynamic collection slug
|
|
158
|
+
user = await payload.findByID({
|
|
159
|
+
collection: authSlug,
|
|
160
|
+
id: userId,
|
|
161
|
+
overrideAccess: true
|
|
162
|
+
});
|
|
163
|
+
} catch {
|
|
164
|
+
user = undefined;
|
|
165
|
+
}
|
|
166
|
+
const totpField = user?.totp ?? undefined;
|
|
167
|
+
if (!totpField?.enabled || typeof totpField.secret !== 'string') {
|
|
168
|
+
return {
|
|
169
|
+
ok: false,
|
|
170
|
+
code: 'plugin_unavailable',
|
|
171
|
+
message: "Two-factor authentication isn't set up for your account. Go to Account → Security to enrol your authenticator app."
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
let plainSecret;
|
|
175
|
+
try {
|
|
176
|
+
plainSecret = decryptFn(totpField.secret);
|
|
177
|
+
} catch {
|
|
178
|
+
return {
|
|
179
|
+
ok: false,
|
|
180
|
+
code: 'plugin_unavailable',
|
|
181
|
+
message: 'Your two-factor authentication setup is unreadable on the server. Contact engineering.'
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
const result = verifyFn(plainSecret, totpCode, totpField.lastCounter);
|
|
185
|
+
if (!result.valid) {
|
|
186
|
+
return {
|
|
187
|
+
ok: false,
|
|
188
|
+
code: 'invalid',
|
|
189
|
+
message: 'The authentication code was incorrect. Check your authenticator app and try again.'
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
ok: true
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
// Config helpers
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
export function getAiTranslateConfig(payload) {
|
|
200
|
+
return payload.config?.custom?.aiTranslate;
|
|
201
|
+
}
|
|
202
|
+
export function getBulkConfig(payload) {
|
|
203
|
+
const cfg = getAiTranslateConfig(payload);
|
|
204
|
+
return cfg?.bulk;
|
|
205
|
+
}
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
// Body parsing
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
/**
|
|
210
|
+
* Reads a JSON body using the same `req.json()` pattern existing
|
|
211
|
+
* handlers in this directory rely on. Returns either the parsed body
|
|
212
|
+
* or an `errorResponse` Response object so the handler can `return`
|
|
213
|
+
* it directly.
|
|
214
|
+
*
|
|
215
|
+
* Returning a Response (instead of throwing) keeps the handler
|
|
216
|
+
* flat — no try/catch around an awaited body read at every call
|
|
217
|
+
* site.
|
|
218
|
+
*/ export async function readJsonBody(req) {
|
|
219
|
+
try {
|
|
220
|
+
if (typeof req.json !== 'function') {
|
|
221
|
+
return {
|
|
222
|
+
ok: true,
|
|
223
|
+
body: {}
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
const body = await req.json();
|
|
227
|
+
return {
|
|
228
|
+
ok: true,
|
|
229
|
+
body: body ?? {}
|
|
230
|
+
};
|
|
231
|
+
} catch {
|
|
232
|
+
return {
|
|
233
|
+
ok: false,
|
|
234
|
+
res: errorResponse('invalid_json', 'Invalid JSON body', 400)
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
// Pagination cursor — opaque base64 of `{ offset: number }`
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
/**
|
|
242
|
+
* The status endpoint paginates unit drill-down via opaque cursors.
|
|
243
|
+
* The internal representation is `{ offset }` — base64-encoded so
|
|
244
|
+
* clients can't poke at it and depend on internal shape. Cheap and
|
|
245
|
+
* stable; switching to a row-id cursor later is a non-breaking change
|
|
246
|
+
* because the surface is opaque.
|
|
247
|
+
*/ export function encodeCursor(offset) {
|
|
248
|
+
return Buffer.from(JSON.stringify({
|
|
249
|
+
offset
|
|
250
|
+
}), 'utf-8').toString('base64');
|
|
251
|
+
}
|
|
252
|
+
export function decodeCursor(cursor) {
|
|
253
|
+
if (!cursor) return 0;
|
|
254
|
+
try {
|
|
255
|
+
const raw = Buffer.from(cursor, 'base64').toString('utf-8');
|
|
256
|
+
const parsed = JSON.parse(raw);
|
|
257
|
+
if (typeof parsed.offset === 'number' && Number.isFinite(parsed.offset) && parsed.offset >= 0) {
|
|
258
|
+
return Math.floor(parsed.offset);
|
|
259
|
+
}
|
|
260
|
+
} catch {
|
|
261
|
+
// fall through
|
|
262
|
+
}
|
|
263
|
+
return 0;
|
|
264
|
+
}
|
|
265
|
+
// ---------------------------------------------------------------------------
|
|
266
|
+
// Audit-log entry (best-effort)
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
268
|
+
/**
|
|
269
|
+
* Decision #33 mandates an audit-log entry on revert. We don't take a
|
|
270
|
+
* hard dependency on `auditLogPlugin` — instead we write directly to
|
|
271
|
+
* the audit-logs collection if it exists. Best-effort: failures are
|
|
272
|
+
* logged but don't abort the action.
|
|
273
|
+
*/ export async function writeBulkAuditEntry(payload, params) {
|
|
274
|
+
const collections = payload.config?.collections ?? [];
|
|
275
|
+
const auditSlug = collections.find((c)=>c.slug === 'audit-logs')?.slug;
|
|
276
|
+
if (!auditSlug) return;
|
|
277
|
+
try {
|
|
278
|
+
// biome-ignore lint/suspicious/noExplicitAny: dynamic collection slug
|
|
279
|
+
await payload.create({
|
|
280
|
+
collection: auditSlug,
|
|
281
|
+
data: {
|
|
282
|
+
authorEmail: params.userEmail ?? String(params.userId),
|
|
283
|
+
action: params.action,
|
|
284
|
+
type: 'audit',
|
|
285
|
+
source: 'bulk-translate',
|
|
286
|
+
collectionSlug: 'bulk-translate-batches',
|
|
287
|
+
documentId: params.batchId,
|
|
288
|
+
documentTitle: `bulk-translate-${params.batchId}`,
|
|
289
|
+
user: params.userId,
|
|
290
|
+
changes: params.metadata ?? null
|
|
291
|
+
},
|
|
292
|
+
overrideAccess: true
|
|
293
|
+
});
|
|
294
|
+
} catch (err) {
|
|
295
|
+
payload.logger?.warn?.(`[ai-translate] bulk-translate: audit-log write failed for ${params.action} on batch ${params.batchId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { PayloadHandler } from 'payload';
|
|
2
|
+
export interface BulkActiveHandlerOptions {
|
|
3
|
+
batchesCollectionSlug?: string;
|
|
4
|
+
unitsCollectionSlug?: string;
|
|
5
|
+
/**
|
|
6
|
+
* Window after a batch's `completedAt` during which the terminal card
|
|
7
|
+
* still shows on the Hub. Defaults to 24 hours, mirroring Decision #28.
|
|
8
|
+
*/
|
|
9
|
+
terminalVisibilityMs?: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* `GET /api/translation-hub/bulk-translate/active`
|
|
13
|
+
*
|
|
14
|
+
* Returns the most relevant batch for the Hub: an in-flight batch
|
|
15
|
+
* (queued / running / cancelling) takes precedence; otherwise we
|
|
16
|
+
* return the most recently-terminal batch within the 24h visibility
|
|
17
|
+
* window so the operator can review or revert it.
|
|
18
|
+
*
|
|
19
|
+
* Response shape: `{ batch: BulkTranslateBatchSummary | null }`.
|
|
20
|
+
*/
|
|
21
|
+
export declare const getBulkTranslateActiveHandler: (options?: BulkActiveHandlerOptions) => PayloadHandler;
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { DEFAULT_BULK_TRANSLATE_BATCHES_COLLECTION_SLUG } from '../../bulk-translate-batches-collection.js';
|
|
2
|
+
import { DEFAULT_BULK_TRANSLATE_UNITS_COLLECTION_SLUG } from '../../bulk-translate-units-collection.js';
|
|
3
|
+
import { computeLiveBatchCount } from '../../lib/batch-counts.js';
|
|
4
|
+
import { getDrizzle, slugToTable } from '../../lib/bulk-translate-migrations.js';
|
|
5
|
+
import { createScopedLogger } from '../../lib/logger.js';
|
|
6
|
+
import { errorResponse, isEditorOrAdmin, unauthorizedResponse } from './_helpers.js';
|
|
7
|
+
const DEFAULT_TERMINAL_VISIBILITY_MS = 24 * 60 * 60 * 1000;
|
|
8
|
+
/**
|
|
9
|
+
* `GET /api/translation-hub/bulk-translate/active`
|
|
10
|
+
*
|
|
11
|
+
* Returns the most relevant batch for the Hub: an in-flight batch
|
|
12
|
+
* (queued / running / cancelling) takes precedence; otherwise we
|
|
13
|
+
* return the most recently-terminal batch within the 24h visibility
|
|
14
|
+
* window so the operator can review or revert it.
|
|
15
|
+
*
|
|
16
|
+
* Response shape: `{ batch: BulkTranslateBatchSummary | null }`.
|
|
17
|
+
*/ export const getBulkTranslateActiveHandler = (options = {})=>async (req)=>{
|
|
18
|
+
const batchesSlug = options.batchesCollectionSlug ?? DEFAULT_BULK_TRANSLATE_BATCHES_COLLECTION_SLUG;
|
|
19
|
+
const unitsSlug = options.unitsCollectionSlug ?? DEFAULT_BULK_TRANSLATE_UNITS_COLLECTION_SLUG;
|
|
20
|
+
const visibilityMs = options.terminalVisibilityMs ?? DEFAULT_TERMINAL_VISIBILITY_MS;
|
|
21
|
+
if (!req.user) {
|
|
22
|
+
return unauthorizedResponse(req);
|
|
23
|
+
}
|
|
24
|
+
if (!isEditorOrAdmin(req.user)) {
|
|
25
|
+
return errorResponse('forbidden', "You don't have permission to view bulk-translation status. Contact an admin.", 403);
|
|
26
|
+
}
|
|
27
|
+
// Prefer the most recent active batch. Active here = anything that
|
|
28
|
+
// isn't terminal — a single tenant typically only has one in flight
|
|
29
|
+
// (the coordinator's lease lock enforces this), so the limit=1 sort
|
|
30
|
+
// is enough.
|
|
31
|
+
let active;
|
|
32
|
+
try {
|
|
33
|
+
const result = await req.payload.find({
|
|
34
|
+
collection: batchesSlug,
|
|
35
|
+
where: {
|
|
36
|
+
status: {
|
|
37
|
+
in: [
|
|
38
|
+
'queued',
|
|
39
|
+
'running',
|
|
40
|
+
'cancelling'
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
sort: '-enqueuedAt',
|
|
45
|
+
limit: 1,
|
|
46
|
+
depth: 0,
|
|
47
|
+
overrideAccess: true
|
|
48
|
+
});
|
|
49
|
+
active = result.docs[0];
|
|
50
|
+
} catch (err) {
|
|
51
|
+
const log = createScopedLogger(req.payload, {
|
|
52
|
+
component: 'hub.active'
|
|
53
|
+
});
|
|
54
|
+
log.event('warn', 'hub.active.batch-read.failed', {
|
|
55
|
+
err,
|
|
56
|
+
endpoint: 'hub.active',
|
|
57
|
+
phase: 'in-flight'
|
|
58
|
+
});
|
|
59
|
+
active = undefined;
|
|
60
|
+
}
|
|
61
|
+
// No active batch — look for the most recent terminal batch within
|
|
62
|
+
// the visibility window so the operator can still see / revert it.
|
|
63
|
+
let chosen = active;
|
|
64
|
+
if (!chosen) {
|
|
65
|
+
const cutoff = new Date(Date.now() - visibilityMs).toISOString();
|
|
66
|
+
try {
|
|
67
|
+
const result = await req.payload.find({
|
|
68
|
+
collection: batchesSlug,
|
|
69
|
+
where: {
|
|
70
|
+
and: [
|
|
71
|
+
{
|
|
72
|
+
status: {
|
|
73
|
+
in: [
|
|
74
|
+
'success',
|
|
75
|
+
'failed',
|
|
76
|
+
'partial',
|
|
77
|
+
'reverted',
|
|
78
|
+
'cancelled'
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
completedAt: {
|
|
84
|
+
greater_than: cutoff
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
},
|
|
89
|
+
sort: '-completedAt',
|
|
90
|
+
limit: 1,
|
|
91
|
+
depth: 0,
|
|
92
|
+
overrideAccess: true
|
|
93
|
+
});
|
|
94
|
+
chosen = result.docs[0];
|
|
95
|
+
} catch (err) {
|
|
96
|
+
const log = createScopedLogger(req.payload, {
|
|
97
|
+
component: 'hub.active'
|
|
98
|
+
});
|
|
99
|
+
log.event('warn', 'hub.active.batch-read.failed', {
|
|
100
|
+
err,
|
|
101
|
+
endpoint: 'hub.active',
|
|
102
|
+
phase: 'terminal-window'
|
|
103
|
+
});
|
|
104
|
+
chosen = undefined;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (!chosen) {
|
|
108
|
+
return Response.json({
|
|
109
|
+
batch: null
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
const collections = await aggregateCollectionsBreakdown(req.payload, unitsSlug, String(chosen.id));
|
|
113
|
+
// Live counts — bypass cached `completedUnits` / `failedUnits` on
|
|
114
|
+
// the batch row which drift on retries. See `lib/batch-counts.ts`.
|
|
115
|
+
const live = await computeLiveBatchCount(req.payload, unitsSlug, String(chosen.id));
|
|
116
|
+
const completedAtMs = chosen.completedAt ? Date.parse(chosen.completedAt) : null;
|
|
117
|
+
const revertExpiresAt = completedAtMs !== null ? new Date(completedAtMs + visibilityMs).toISOString() : null;
|
|
118
|
+
return Response.json({
|
|
119
|
+
batch: {
|
|
120
|
+
id: String(chosen.id),
|
|
121
|
+
status: chosen.status,
|
|
122
|
+
mode: chosen.mode,
|
|
123
|
+
totalUnits: live.total > 0 ? live.total : Number(chosen.totalUnits ?? 0),
|
|
124
|
+
completedUnits: live.completed,
|
|
125
|
+
failedUnits: live.failed,
|
|
126
|
+
estimatedCostUsd: chosen.estimatedCostUsd !== undefined ? Number(chosen.estimatedCostUsd) : undefined,
|
|
127
|
+
actualCostUsd: chosen.actualCostUsd !== undefined ? Number(chosen.actualCostUsd) : undefined,
|
|
128
|
+
createdAt: chosen.createdAt ?? chosen.enqueuedAt ?? new Date(0).toISOString(),
|
|
129
|
+
startedAt: chosen.startedAt ?? null,
|
|
130
|
+
completedAt: chosen.completedAt ?? null,
|
|
131
|
+
etaSeconds: null,
|
|
132
|
+
collections,
|
|
133
|
+
revertExpiresAt,
|
|
134
|
+
revertedAt: chosen.revertedAt ?? null,
|
|
135
|
+
triggeredByEmail: chosen.triggeredByEmail ?? null,
|
|
136
|
+
// 1.2.8: editor-facing chip needs the raw user id to compare
|
|
137
|
+
// against the viewer's id and decide whether to say "Your bulk
|
|
138
|
+
// run" or "Bulk run by alice@…".
|
|
139
|
+
triggeredByUserId: chosen.triggeredByUserId != null ? String(chosen.triggeredByUserId) : null
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* Per-collection progress for the in-flight monitor. We aggregate by
|
|
145
|
+
* counting unit rows per `collection` value; the labels are echoed
|
|
146
|
+
* back as the slug since the endpoint has no view of collection
|
|
147
|
+
* `labels` (the UI maps slug → human label client-side).
|
|
148
|
+
*/ async function aggregateCollectionsBreakdown(payload, unitsSlug, batchId) {
|
|
149
|
+
const buckets = new Map();
|
|
150
|
+
const tally = (slug, status, n)=>{
|
|
151
|
+
if (!slug) return;
|
|
152
|
+
const bucket = buckets.get(slug) ?? {
|
|
153
|
+
total: 0,
|
|
154
|
+
completed: 0,
|
|
155
|
+
failed: 0
|
|
156
|
+
};
|
|
157
|
+
bucket.total += n;
|
|
158
|
+
if (status === 'success') bucket.completed += n;
|
|
159
|
+
if (status === 'failed') bucket.failed += n;
|
|
160
|
+
buckets.set(slug, bucket);
|
|
161
|
+
};
|
|
162
|
+
// Fast path: GROUP BY on Postgres. This endpoint is polled every 5s
|
|
163
|
+
// by every open Hub tab — and (terminal-visibility) for a full 24h
|
|
164
|
+
// after a run completes. The previous fetch-all pulled every unit
|
|
165
|
+
// row INCLUDING the `pre_run_snapshot` jsonb on each poll; on prod-
|
|
166
|
+
// sized batches that alone was seconds of DB + JSON.parse work per
|
|
167
|
+
// poll (2026-06-10 outage amplifier).
|
|
168
|
+
let aggregated = false;
|
|
169
|
+
if (/^\d+$/.test(batchId)) {
|
|
170
|
+
const db = getDrizzle(payload);
|
|
171
|
+
if (db) {
|
|
172
|
+
try {
|
|
173
|
+
const res = await db.execute(`SELECT collection, status, count(*) AS n
|
|
174
|
+
FROM ${slugToTable(unitsSlug)}
|
|
175
|
+
WHERE batch_id_id = ${batchId}
|
|
176
|
+
GROUP BY 1, 2`);
|
|
177
|
+
for (const row of res?.rows ?? []){
|
|
178
|
+
tally(String(row.collection ?? ''), String(row.status ?? ''), Number(row.n ?? 0));
|
|
179
|
+
}
|
|
180
|
+
aggregated = true;
|
|
181
|
+
} catch (err) {
|
|
182
|
+
payload.logger?.warn?.(`[ai-translate] active: grouped breakdown SQL failed, falling back to row scan: ${err instanceof Error ? err.message : String(err)}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (!aggregated) {
|
|
187
|
+
// Fallback: projected row scan — (collection, status) only, so the
|
|
188
|
+
// snapshot jsonb never leaves the database.
|
|
189
|
+
let page = 1;
|
|
190
|
+
// eslint-disable-next-line no-constant-condition
|
|
191
|
+
while(true){
|
|
192
|
+
const result = await payload.find({
|
|
193
|
+
collection: unitsSlug,
|
|
194
|
+
where: {
|
|
195
|
+
batchId: {
|
|
196
|
+
equals: batchId
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
page,
|
|
200
|
+
limit: 5000,
|
|
201
|
+
depth: 0,
|
|
202
|
+
select: {
|
|
203
|
+
collection: true,
|
|
204
|
+
status: true
|
|
205
|
+
},
|
|
206
|
+
overrideAccess: true
|
|
207
|
+
});
|
|
208
|
+
for (const row of result.docs){
|
|
209
|
+
tally(String(row.collection ?? ''), String(row.status ?? ''), 1);
|
|
210
|
+
}
|
|
211
|
+
if (!result.hasNextPage) break;
|
|
212
|
+
page += 1;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return Array.from(buckets.entries()).map(([slug, b])=>({
|
|
216
|
+
slug,
|
|
217
|
+
label: slug,
|
|
218
|
+
...b
|
|
219
|
+
}));
|
|
220
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { PayloadHandler } from 'payload';
|
|
2
|
+
export interface BulkCancelHandlerOptions {
|
|
3
|
+
batchesCollectionSlug?: string;
|
|
4
|
+
unitsCollectionSlug?: string;
|
|
5
|
+
/** Payload jobs collection slug. Default `'payload-jobs'`. */
|
|
6
|
+
jobsCollectionSlug?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* `POST /api/translation-hub/bulk-translate/:id/cancel`
|
|
10
|
+
*
|
|
11
|
+
* Transitions a queued/running batch to `cancelling`. The coordinator
|
|
12
|
+
* and worker tasks honor the new status at their next checkpoint
|
|
13
|
+
* (coordinator reads `batch.status` at the top of every tick; worker
|
|
14
|
+
* reads at terminal write). In-flight LLM calls finish naturally —
|
|
15
|
+
* we don't try to interrupt them mid-stream.
|
|
16
|
+
*
|
|
17
|
+
* Queued payload-jobs rows for this batch are best-effort cancelled
|
|
18
|
+
* here (we update them to a terminal state so the runner skips them).
|
|
19
|
+
* Some Payload versions expose a jobs API for this; we fall back to a
|
|
20
|
+
* direct update on the queued rows when the API isn't available.
|
|
21
|
+
*/
|
|
22
|
+
export declare const getBulkTranslateCancelHandler: (options?: BulkCancelHandlerOptions) => PayloadHandler;
|