@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,144 @@
|
|
|
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 { createScopedLogger } from '../../lib/logger.js';
|
|
4
|
+
import { errorResponse, isAdminUser, unauthorizedResponse } from './_helpers.js';
|
|
5
|
+
import { extractBatchId } from './status.js';
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Handler
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
/**
|
|
10
|
+
* `POST /api/translation-hub/bulk-translate/:id/force-reset`
|
|
11
|
+
*
|
|
12
|
+
* Recovery action — F-SEC-CRON-RESET. Marks every `running` unit in
|
|
13
|
+
* the batch as `failed` with `failureCode: 'transient.crashed'` and
|
|
14
|
+
* the batch itself as `failed`. Used by on-call when the worker /
|
|
15
|
+
* coordinator stalled and the normal janitor sweep hasn't picked it
|
|
16
|
+
* up.
|
|
17
|
+
*
|
|
18
|
+
* Auth: admin only — explicitly NO TOTP gate (this is a recovery
|
|
19
|
+
* action; admins must be able to unblock production without a TOTP
|
|
20
|
+
* dance if their mobile device is unavailable). The daily USD cap
|
|
21
|
+
* already enforced spending bounds for the batch in question.
|
|
22
|
+
*/ export const getBulkTranslateForceResetHandler = (options = {})=>async (req)=>{
|
|
23
|
+
const batchesSlug = options.batchesCollectionSlug ?? DEFAULT_BULK_TRANSLATE_BATCHES_COLLECTION_SLUG;
|
|
24
|
+
const unitsSlug = options.unitsCollectionSlug ?? DEFAULT_BULK_TRANSLATE_UNITS_COLLECTION_SLUG;
|
|
25
|
+
if (!req.user) {
|
|
26
|
+
return unauthorizedResponse(req);
|
|
27
|
+
}
|
|
28
|
+
if (!isAdminUser(req.user)) {
|
|
29
|
+
return errorResponse('forbidden', 'Admin role required to force-reset a bulk-translation batch.', 403);
|
|
30
|
+
}
|
|
31
|
+
const batchId = extractBatchId(req.url ?? '');
|
|
32
|
+
if (!batchId) {
|
|
33
|
+
return errorResponse('invalid_batch_id', 'Batch ID could not be parsed from the request URL.', 400);
|
|
34
|
+
}
|
|
35
|
+
let batch;
|
|
36
|
+
try {
|
|
37
|
+
batch = await req.payload.findByID({
|
|
38
|
+
collection: batchesSlug,
|
|
39
|
+
id: batchId,
|
|
40
|
+
overrideAccess: true,
|
|
41
|
+
depth: 0
|
|
42
|
+
});
|
|
43
|
+
} catch (err) {
|
|
44
|
+
const log = createScopedLogger(req.payload, {
|
|
45
|
+
component: 'hub.force-reset',
|
|
46
|
+
batchId
|
|
47
|
+
});
|
|
48
|
+
log.event('warn', 'hub.force-reset.batch-read.failed', {
|
|
49
|
+
err,
|
|
50
|
+
endpoint: 'hub.force-reset',
|
|
51
|
+
batchId
|
|
52
|
+
});
|
|
53
|
+
batch = undefined;
|
|
54
|
+
}
|
|
55
|
+
if (!batch) {
|
|
56
|
+
return errorResponse('not_found', 'This translation run no longer exists. Refresh the page.', 404);
|
|
57
|
+
}
|
|
58
|
+
// Reset every `running` unit. The batch may also have `pending`
|
|
59
|
+
// units (queued payload-jobs rows that never dequeued because of
|
|
60
|
+
// a coordinator stall) — those are reset too, since the operator
|
|
61
|
+
// is explicitly unblocking the batch.
|
|
62
|
+
let resetUnits = 0;
|
|
63
|
+
const RESET_STATUSES = [
|
|
64
|
+
'running',
|
|
65
|
+
'pending'
|
|
66
|
+
];
|
|
67
|
+
for (const status of RESET_STATUSES){
|
|
68
|
+
let page = 1;
|
|
69
|
+
// eslint-disable-next-line no-constant-condition
|
|
70
|
+
while(true){
|
|
71
|
+
const result = await req.payload.find({
|
|
72
|
+
collection: unitsSlug,
|
|
73
|
+
where: {
|
|
74
|
+
and: [
|
|
75
|
+
{
|
|
76
|
+
batchId: {
|
|
77
|
+
equals: batchId
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
status: {
|
|
82
|
+
equals: status
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
]
|
|
86
|
+
},
|
|
87
|
+
page,
|
|
88
|
+
limit: 1000,
|
|
89
|
+
depth: 0,
|
|
90
|
+
overrideAccess: true
|
|
91
|
+
});
|
|
92
|
+
for (const u of result.docs){
|
|
93
|
+
try {
|
|
94
|
+
await req.payload.update({
|
|
95
|
+
collection: unitsSlug,
|
|
96
|
+
id: u.id,
|
|
97
|
+
data: {
|
|
98
|
+
status: 'failed',
|
|
99
|
+
failureCode: 'transient.crashed',
|
|
100
|
+
failureMessage: 'force-reset by admin',
|
|
101
|
+
completedAt: new Date().toISOString()
|
|
102
|
+
},
|
|
103
|
+
overrideAccess: true
|
|
104
|
+
});
|
|
105
|
+
resetUnits += 1;
|
|
106
|
+
} catch (err) {
|
|
107
|
+
req.payload.logger?.warn?.(`[ai-translate] force-reset: failed to reset unit ${String(u.id)}: ${err instanceof Error ? err.message : String(err)}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (!result.hasNextPage) break;
|
|
111
|
+
page += 1;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Mark batch itself as failed. We don't transition to `cancelled`
|
|
115
|
+
// because force-reset is an abnormal termination, not an admin-
|
|
116
|
+
// initiated cancel — the distinction matters for downstream
|
|
117
|
+
// alerting / analytics.
|
|
118
|
+
try {
|
|
119
|
+
await req.payload.update({
|
|
120
|
+
collection: batchesSlug,
|
|
121
|
+
id: batchId,
|
|
122
|
+
data: {
|
|
123
|
+
status: 'failed',
|
|
124
|
+
completedAt: new Date().toISOString(),
|
|
125
|
+
failures: {
|
|
126
|
+
...batch.failures ?? {},
|
|
127
|
+
force_reset_by: String(req.user.id),
|
|
128
|
+
force_reset_note: 'force-reset by admin'
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
overrideAccess: true
|
|
132
|
+
});
|
|
133
|
+
} catch (err) {
|
|
134
|
+
req.payload.logger?.warn?.(`[ai-translate] force-reset: failed to mark batch failed for ${batchId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
135
|
+
return errorResponse('reset_failed', "The reset couldn't be saved. Try again, or contact engineering if it keeps happening.", 500);
|
|
136
|
+
}
|
|
137
|
+
return Response.json({
|
|
138
|
+
data: {
|
|
139
|
+
batchId,
|
|
140
|
+
resetUnits,
|
|
141
|
+
status: 'failed'
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translation Hub bulk-translate REST endpoints (PR2 v1.2.0).
|
|
3
|
+
*
|
|
4
|
+
* Public surface mounted at `/api/translation-hub/bulk-translate/...`
|
|
5
|
+
* by the plugin's central wiring step (`plugin.ts`). Each handler is
|
|
6
|
+
* a factory that takes optional slug/task overrides for testability;
|
|
7
|
+
* production wiring uses defaults.
|
|
8
|
+
*
|
|
9
|
+
* Reference: docs/tickets/Plan-2026-05-27-bulk-translate.md §4.
|
|
10
|
+
*/
|
|
11
|
+
export { type BulkActiveHandlerOptions, getBulkTranslateActiveHandler, } from './active.js';
|
|
12
|
+
export { type BulkCancelHandlerOptions, getBulkTranslateCancelHandler, } from './cancel.js';
|
|
13
|
+
export { type BulkEnqueueBody, type BulkEnqueueHandlerOptions, type BulkEnqueueScope, getBulkTranslateEnqueueHandler, } from './enqueue.js';
|
|
14
|
+
export { type BulkFailuresHandlerOptions, getBulkTranslateFailuresHandler, } from './failures.js';
|
|
15
|
+
export { type BulkForceResetHandlerOptions, getBulkTranslateForceResetHandler, } from './force-reset.js';
|
|
16
|
+
export { type BulkListHandlerOptions, getBulkTranslateListHandler, } from './list.js';
|
|
17
|
+
export { type BulkPreflightHandlerOptions, getBulkTranslatePreflightHandler, } from './preflight.js';
|
|
18
|
+
export { type BulkRetryFailedBody, type BulkRetryFailedHandlerOptions, getBulkTranslateRetryFailedHandler, } from './retry-failed.js';
|
|
19
|
+
export { type BulkRevertBody, type BulkRevertHandlerOptions, type BulkRevertWarning, getBulkTranslateRevertHandler, } from './revert.js';
|
|
20
|
+
export { type BatchJobSummary, type BulkStatusHandlerOptions, extractBatchId, getBulkTranslateStatusHandler, } from './status.js';
|
|
21
|
+
export { getTranslationHubUsageSummaryHandler, parseRangeParams, type UsageSummaryCollectionBucket, type UsageSummaryHandlerOptions, type UsageSummaryLocaleBucket, type UsageSummaryModelBucket, type UsageSummaryRange, type UsageSummaryResponse, type UsageSummarySampleRow, type UsageSummaryTotals, } from './usage-summary.js';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translation Hub bulk-translate REST endpoints (PR2 v1.2.0).
|
|
3
|
+
*
|
|
4
|
+
* Public surface mounted at `/api/translation-hub/bulk-translate/...`
|
|
5
|
+
* by the plugin's central wiring step (`plugin.ts`). Each handler is
|
|
6
|
+
* a factory that takes optional slug/task overrides for testability;
|
|
7
|
+
* production wiring uses defaults.
|
|
8
|
+
*
|
|
9
|
+
* Reference: docs/tickets/Plan-2026-05-27-bulk-translate.md §4.
|
|
10
|
+
*/ export { getBulkTranslateActiveHandler } from './active.js';
|
|
11
|
+
export { getBulkTranslateCancelHandler } from './cancel.js';
|
|
12
|
+
export { getBulkTranslateEnqueueHandler } from './enqueue.js';
|
|
13
|
+
export { getBulkTranslateFailuresHandler } from './failures.js';
|
|
14
|
+
export { getBulkTranslateForceResetHandler } from './force-reset.js';
|
|
15
|
+
export { getBulkTranslateListHandler } from './list.js';
|
|
16
|
+
export { getBulkTranslatePreflightHandler } from './preflight.js';
|
|
17
|
+
export { getBulkTranslateRetryFailedHandler } from './retry-failed.js';
|
|
18
|
+
export { getBulkTranslateRevertHandler } from './revert.js';
|
|
19
|
+
export { extractBatchId, getBulkTranslateStatusHandler } from './status.js';
|
|
20
|
+
export { getTranslationHubUsageSummaryHandler, parseRangeParams } from './usage-summary.js';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { PayloadHandler } from 'payload';
|
|
2
|
+
export interface BulkListHandlerOptions {
|
|
3
|
+
batchesCollectionSlug?: string;
|
|
4
|
+
unitsCollectionSlug?: string;
|
|
5
|
+
/**
|
|
6
|
+
* Window after a batch's `completedAt` during which Revert is still
|
|
7
|
+
* allowed. Defaults to 24h, mirroring the same constant in
|
|
8
|
+
* `active.ts` (Decision #28). Exported as `revertExpiresAt` on each
|
|
9
|
+
* row so the UI can render the countdown without a second query.
|
|
10
|
+
*/
|
|
11
|
+
revertWindowMs?: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* `GET /api/translation-hub/bulk-translate`
|
|
15
|
+
*
|
|
16
|
+
* Paginated list of every bulk-translate batch the operator can see,
|
|
17
|
+
* sorted by `enqueuedAt DESC`. Powers the Bulk Translate Runs hub
|
|
18
|
+
* (see `views/BulkRunsHub`).
|
|
19
|
+
*
|
|
20
|
+
* Query params:
|
|
21
|
+
* - `status` — comma-separated list (active|terminal|queued|running|
|
|
22
|
+
* cancelling|success|failed|partial|reverted|cancelled). The
|
|
23
|
+
* aliases `active` and `terminal` expand to the full status sets.
|
|
24
|
+
* - `mode` — changed|force|canary
|
|
25
|
+
* - `triggeredBy` — email exact match
|
|
26
|
+
* - `since`, `until` — ISO timestamps on `enqueuedAt`
|
|
27
|
+
* - `hasFailures` — `true` filters to batches with failed_units > 0
|
|
28
|
+
* - `cursor` — opaque base64 from the previous response
|
|
29
|
+
* - `limit` — page size (default 20, max 100)
|
|
30
|
+
*
|
|
31
|
+
* Response shape:
|
|
32
|
+
* `{ batches: BulkTranslateBatchSummary[], nextCursor: string | null,
|
|
33
|
+
* total: number }`
|
|
34
|
+
*
|
|
35
|
+
* The per-collection progress breakdown is NOT included here for
|
|
36
|
+
* performance; the Runs hub drill-down fetches it via `/:id/status`
|
|
37
|
+
* when a row is expanded. `collections: []` is returned for type
|
|
38
|
+
* compatibility with the shared `BulkTranslateBatchSummary` shape.
|
|
39
|
+
*/
|
|
40
|
+
export declare const getBulkTranslateListHandler: (options?: BulkListHandlerOptions) => PayloadHandler;
|
|
@@ -0,0 +1,182 @@
|
|
|
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 { computeLiveBatchCounts } from '../../lib/batch-counts.js';
|
|
4
|
+
import { decodeCursor, encodeCursor, errorResponse, isEditorOrAdmin, unauthorizedResponse } from './_helpers.js';
|
|
5
|
+
const DEFAULT_REVERT_WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
6
|
+
const ACTIVE_STATUSES = [
|
|
7
|
+
'queued',
|
|
8
|
+
'running',
|
|
9
|
+
'cancelling'
|
|
10
|
+
];
|
|
11
|
+
const TERMINAL_STATUSES = [
|
|
12
|
+
'success',
|
|
13
|
+
'failed',
|
|
14
|
+
'partial',
|
|
15
|
+
'reverted',
|
|
16
|
+
'cancelled'
|
|
17
|
+
];
|
|
18
|
+
/**
|
|
19
|
+
* `GET /api/translation-hub/bulk-translate`
|
|
20
|
+
*
|
|
21
|
+
* Paginated list of every bulk-translate batch the operator can see,
|
|
22
|
+
* sorted by `enqueuedAt DESC`. Powers the Bulk Translate Runs hub
|
|
23
|
+
* (see `views/BulkRunsHub`).
|
|
24
|
+
*
|
|
25
|
+
* Query params:
|
|
26
|
+
* - `status` — comma-separated list (active|terminal|queued|running|
|
|
27
|
+
* cancelling|success|failed|partial|reverted|cancelled). The
|
|
28
|
+
* aliases `active` and `terminal` expand to the full status sets.
|
|
29
|
+
* - `mode` — changed|force|canary
|
|
30
|
+
* - `triggeredBy` — email exact match
|
|
31
|
+
* - `since`, `until` — ISO timestamps on `enqueuedAt`
|
|
32
|
+
* - `hasFailures` — `true` filters to batches with failed_units > 0
|
|
33
|
+
* - `cursor` — opaque base64 from the previous response
|
|
34
|
+
* - `limit` — page size (default 20, max 100)
|
|
35
|
+
*
|
|
36
|
+
* Response shape:
|
|
37
|
+
* `{ batches: BulkTranslateBatchSummary[], nextCursor: string | null,
|
|
38
|
+
* total: number }`
|
|
39
|
+
*
|
|
40
|
+
* The per-collection progress breakdown is NOT included here for
|
|
41
|
+
* performance; the Runs hub drill-down fetches it via `/:id/status`
|
|
42
|
+
* when a row is expanded. `collections: []` is returned for type
|
|
43
|
+
* compatibility with the shared `BulkTranslateBatchSummary` shape.
|
|
44
|
+
*/ export const getBulkTranslateListHandler = (options = {})=>async (req)=>{
|
|
45
|
+
const batchesSlug = options.batchesCollectionSlug ?? DEFAULT_BULK_TRANSLATE_BATCHES_COLLECTION_SLUG;
|
|
46
|
+
const unitsSlug = options.unitsCollectionSlug ?? DEFAULT_BULK_TRANSLATE_UNITS_COLLECTION_SLUG;
|
|
47
|
+
const revertWindowMs = options.revertWindowMs ?? DEFAULT_REVERT_WINDOW_MS;
|
|
48
|
+
if (!req.user) {
|
|
49
|
+
return unauthorizedResponse(req);
|
|
50
|
+
}
|
|
51
|
+
if (!isEditorOrAdmin(req.user)) {
|
|
52
|
+
return errorResponse('forbidden', "You don't have permission to view bulk-translation history. Contact an admin.", 403);
|
|
53
|
+
}
|
|
54
|
+
const url = new URL(req.url ?? 'http://localhost');
|
|
55
|
+
const params = url.searchParams;
|
|
56
|
+
const limitRaw = params.get('limit');
|
|
57
|
+
let limit = limitRaw ? Number.parseInt(limitRaw, 10) : 20;
|
|
58
|
+
if (!Number.isFinite(limit) || limit <= 0) limit = 20;
|
|
59
|
+
if (limit > 100) limit = 100;
|
|
60
|
+
const offset = decodeCursor(params.get('cursor') ?? undefined);
|
|
61
|
+
const page = Math.floor(offset / limit) + 1;
|
|
62
|
+
const andClauses = [];
|
|
63
|
+
// status filter — expand aliases
|
|
64
|
+
const statusParam = params.get('status');
|
|
65
|
+
if (statusParam) {
|
|
66
|
+
const raw = statusParam.split(',').map((s)=>s.trim()).filter(Boolean);
|
|
67
|
+
const expanded = new Set();
|
|
68
|
+
for (const s of raw){
|
|
69
|
+
if (s === 'active') {
|
|
70
|
+
for (const x of ACTIVE_STATUSES)expanded.add(x);
|
|
71
|
+
} else if (s === 'terminal') {
|
|
72
|
+
for (const x of TERMINAL_STATUSES)expanded.add(x);
|
|
73
|
+
} else {
|
|
74
|
+
expanded.add(s);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (expanded.size > 0) {
|
|
78
|
+
andClauses.push({
|
|
79
|
+
status: {
|
|
80
|
+
in: Array.from(expanded)
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const modeParam = params.get('mode');
|
|
86
|
+
if (modeParam) {
|
|
87
|
+
andClauses.push({
|
|
88
|
+
mode: {
|
|
89
|
+
equals: modeParam
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
const triggeredByParam = params.get('triggeredBy');
|
|
94
|
+
if (triggeredByParam) {
|
|
95
|
+
andClauses.push({
|
|
96
|
+
triggeredByEmail: {
|
|
97
|
+
equals: triggeredByParam
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
const sinceParam = params.get('since');
|
|
102
|
+
if (sinceParam) {
|
|
103
|
+
andClauses.push({
|
|
104
|
+
enqueuedAt: {
|
|
105
|
+
greater_than_equal: sinceParam
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
const untilParam = params.get('until');
|
|
110
|
+
if (untilParam) {
|
|
111
|
+
andClauses.push({
|
|
112
|
+
enqueuedAt: {
|
|
113
|
+
less_than_equal: untilParam
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
const hasFailuresParam = params.get('hasFailures');
|
|
118
|
+
if (hasFailuresParam === 'true') {
|
|
119
|
+
andClauses.push({
|
|
120
|
+
failedUnits: {
|
|
121
|
+
greater_than: 0
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
const where = andClauses.length > 0 ? {
|
|
126
|
+
and: andClauses
|
|
127
|
+
} : undefined;
|
|
128
|
+
const result = await req.payload.find({
|
|
129
|
+
collection: batchesSlug,
|
|
130
|
+
where,
|
|
131
|
+
sort: '-enqueuedAt',
|
|
132
|
+
page,
|
|
133
|
+
limit,
|
|
134
|
+
depth: 0,
|
|
135
|
+
overrideAccess: true
|
|
136
|
+
});
|
|
137
|
+
// Live counts per batch — source of truth is `bulk_translate_units`,
|
|
138
|
+
// not the cached `completed_units / failed_units / skipped_units`
|
|
139
|
+
// columns on the batch row (which drift on retries / cancels /
|
|
140
|
+
// admin SDK interventions). See `lib/batch-counts.ts`.
|
|
141
|
+
const batchIds = result.docs.map((r)=>String(r.id));
|
|
142
|
+
const liveCounts = await computeLiveBatchCounts(req.payload, unitsSlug, batchIds);
|
|
143
|
+
const batches = result.docs.map((row)=>{
|
|
144
|
+
const completedAtMs = row.completedAt ? Date.parse(row.completedAt) : null;
|
|
145
|
+
const revertExpiresAt = completedAtMs !== null ? new Date(completedAtMs + revertWindowMs).toISOString() : null;
|
|
146
|
+
const live = liveCounts.get(String(row.id));
|
|
147
|
+
// Use the live unit-derived total when it's > 0 — for in-flight
|
|
148
|
+
// and recently-completed batches the unit rows ARE the source of
|
|
149
|
+
// truth. Fall back to the cached `totalUnits` on the batch row
|
|
150
|
+
// only when the live count is 0 (empty batch / coordinator hasn't
|
|
151
|
+
// run yet) so the user sees the planned size, not "0 / 0".
|
|
152
|
+
const liveTotal = live?.total ?? 0;
|
|
153
|
+
const totalUnits = liveTotal > 0 ? liveTotal : Number(row.totalUnits ?? 0);
|
|
154
|
+
return {
|
|
155
|
+
id: String(row.id),
|
|
156
|
+
status: row.status,
|
|
157
|
+
mode: row.mode,
|
|
158
|
+
totalUnits,
|
|
159
|
+
completedUnits: live?.completed ?? 0,
|
|
160
|
+
failedUnits: live?.failed ?? 0,
|
|
161
|
+
estimatedCostUsd: row.estimatedCostUsd !== undefined ? Number(row.estimatedCostUsd) : undefined,
|
|
162
|
+
actualCostUsd: row.actualCostUsd !== undefined ? Number(row.actualCostUsd) : undefined,
|
|
163
|
+
createdAt: row.createdAt ?? row.enqueuedAt ?? new Date(0).toISOString(),
|
|
164
|
+
startedAt: row.startedAt ?? null,
|
|
165
|
+
completedAt: row.completedAt ?? null,
|
|
166
|
+
etaSeconds: null,
|
|
167
|
+
collections: [],
|
|
168
|
+
revertExpiresAt,
|
|
169
|
+
revertedAt: row.revertedAt ?? null,
|
|
170
|
+
triggeredByEmail: row.triggeredByEmail ?? null,
|
|
171
|
+
// 1.2.8: editor variant uses this to set the "you" chip on
|
|
172
|
+
// rows the viewer owns (`EditorRecentRunsPanel`).
|
|
173
|
+
triggeredByUserId: row.triggeredByUserId != null ? String(row.triggeredByUserId) : null
|
|
174
|
+
};
|
|
175
|
+
});
|
|
176
|
+
const nextCursor = batches.length === limit ? encodeCursor(offset + limit) : null;
|
|
177
|
+
return Response.json({
|
|
178
|
+
batches,
|
|
179
|
+
nextCursor,
|
|
180
|
+
total: result.totalDocs
|
|
181
|
+
});
|
|
182
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { PayloadHandler } from 'payload';
|
|
2
|
+
export interface BulkPreflightHandlerOptions {
|
|
3
|
+
dailySpendCollectionSlug?: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* `GET /api/translation-hub/bulk-translate/preflight`
|
|
7
|
+
*
|
|
8
|
+
* Returns a snapshot of the work the operator would dispatch if they
|
|
9
|
+
* confirmed a bulk-translate run: scope (collections × locales),
|
|
10
|
+
* document counts, today's USD spend vs cap, and whether TOTP is
|
|
11
|
+
* required + enrolled for this user.
|
|
12
|
+
*
|
|
13
|
+
* The cost estimate is intentionally omitted in v1 — the trigger UI
|
|
14
|
+
* tolerates `undefined` (renders a "cost loading" placeholder) and the
|
|
15
|
+
* real estimator path runs at enqueue-time inside the coordinator.
|
|
16
|
+
* Adding a true preflight estimate would re-run the diff + provider
|
|
17
|
+
* pricing fetch on every modal open, which is too expensive.
|
|
18
|
+
*/
|
|
19
|
+
export declare const getBulkTranslatePreflightHandler: (options?: BulkPreflightHandlerOptions) => PayloadHandler;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { DEFAULT_TRANSLATION_DAILY_SPEND_COLLECTION_SLUG } from '../../translation-daily-spend-collection.js';
|
|
2
|
+
import { errorResponse, getAiTranslateConfig, getBulkConfig, isEditorOrAdmin, unauthorizedResponse } from './_helpers.js';
|
|
3
|
+
import { estimateBatchCost, resolveScope } from './enqueue.js';
|
|
4
|
+
/**
|
|
5
|
+
* `GET /api/translation-hub/bulk-translate/preflight`
|
|
6
|
+
*
|
|
7
|
+
* Returns a snapshot of the work the operator would dispatch if they
|
|
8
|
+
* confirmed a bulk-translate run: scope (collections × locales),
|
|
9
|
+
* document counts, today's USD spend vs cap, and whether TOTP is
|
|
10
|
+
* required + enrolled for this user.
|
|
11
|
+
*
|
|
12
|
+
* The cost estimate is intentionally omitted in v1 — the trigger UI
|
|
13
|
+
* tolerates `undefined` (renders a "cost loading" placeholder) and the
|
|
14
|
+
* real estimator path runs at enqueue-time inside the coordinator.
|
|
15
|
+
* Adding a true preflight estimate would re-run the diff + provider
|
|
16
|
+
* pricing fetch on every modal open, which is too expensive.
|
|
17
|
+
*/ export const getBulkTranslatePreflightHandler = (options = {})=>async (req)=>{
|
|
18
|
+
const dailySlug = options.dailySpendCollectionSlug ?? DEFAULT_TRANSLATION_DAILY_SPEND_COLLECTION_SLUG;
|
|
19
|
+
if (!req.user) {
|
|
20
|
+
return unauthorizedResponse(req);
|
|
21
|
+
}
|
|
22
|
+
if (!isEditorOrAdmin(req.user)) {
|
|
23
|
+
return errorResponse('forbidden', "You don't have permission to start a bulk translation. Contact an admin.", 403);
|
|
24
|
+
}
|
|
25
|
+
const config = getAiTranslateConfig(req.payload);
|
|
26
|
+
const bulkConfig = getBulkConfig(req.payload);
|
|
27
|
+
if (!config || !bulkConfig) {
|
|
28
|
+
return errorResponse('not_configured', "Bulk translation isn't set up for this site. Contact engineering.", 404);
|
|
29
|
+
}
|
|
30
|
+
const excludeSet = new Set(bulkConfig.excludeCollections ?? []);
|
|
31
|
+
const collections = (config.collections ?? []).filter((slug)=>!excludeSet.has(slug));
|
|
32
|
+
const globals = (config.globals ?? []).filter((slug)=>!excludeSet.has(slug));
|
|
33
|
+
const targetLocales = config.targetLocales ?? [];
|
|
34
|
+
// Per-collection counts. `total` = docs in the collection;
|
|
35
|
+
// `changed` is a best-effort guess that matches `total` (the real
|
|
36
|
+
// diff-skip evaluation happens inside the coordinator at enqueue
|
|
37
|
+
// time, not preflight). The UI uses these for proportions only.
|
|
38
|
+
const perCollection = [];
|
|
39
|
+
let totalDocs = 0;
|
|
40
|
+
for (const slug of collections){
|
|
41
|
+
try {
|
|
42
|
+
const result = await req.payload.count({
|
|
43
|
+
collection: slug,
|
|
44
|
+
overrideAccess: true
|
|
45
|
+
});
|
|
46
|
+
const count = Number(result.totalDocs ?? 0);
|
|
47
|
+
perCollection.push({
|
|
48
|
+
slug,
|
|
49
|
+
label: slug,
|
|
50
|
+
changed: count,
|
|
51
|
+
total: count
|
|
52
|
+
});
|
|
53
|
+
totalDocs += count;
|
|
54
|
+
} catch {
|
|
55
|
+
perCollection.push({
|
|
56
|
+
slug,
|
|
57
|
+
label: slug,
|
|
58
|
+
changed: 0,
|
|
59
|
+
total: 0
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Globals contribute exactly one "document" each.
|
|
64
|
+
for (const slug of globals){
|
|
65
|
+
perCollection.push({
|
|
66
|
+
slug,
|
|
67
|
+
label: slug,
|
|
68
|
+
changed: 1,
|
|
69
|
+
total: 1
|
|
70
|
+
});
|
|
71
|
+
totalDocs += 1;
|
|
72
|
+
}
|
|
73
|
+
// Daily spend lookup — keyed by ISO date string (YYYY-MM-DD).
|
|
74
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
75
|
+
let spentUsd = 0;
|
|
76
|
+
try {
|
|
77
|
+
const result = await req.payload.find({
|
|
78
|
+
collection: dailySlug,
|
|
79
|
+
where: {
|
|
80
|
+
and: [
|
|
81
|
+
{
|
|
82
|
+
date: {
|
|
83
|
+
equals: today
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
consumerKey: {
|
|
88
|
+
equals: 'default'
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
]
|
|
92
|
+
},
|
|
93
|
+
limit: 1,
|
|
94
|
+
depth: 0,
|
|
95
|
+
overrideAccess: true
|
|
96
|
+
});
|
|
97
|
+
const row = result.docs[0];
|
|
98
|
+
spentUsd = Number(row?.spendUsd ?? 0);
|
|
99
|
+
} catch {
|
|
100
|
+
spentUsd = 0;
|
|
101
|
+
}
|
|
102
|
+
const capUsd = Number(bulkConfig.dailyUsdCap ?? 0);
|
|
103
|
+
const tomorrow = new Date();
|
|
104
|
+
tomorrow.setUTCHours(24, 0, 0, 0);
|
|
105
|
+
const resetsAt = tomorrow.toISOString();
|
|
106
|
+
const requireTotp = bulkConfig.requireTotp === true;
|
|
107
|
+
const userTotp = req.user?.totp;
|
|
108
|
+
const totpEnrolled = !!userTotp?.enabled;
|
|
109
|
+
// Real cost estimate — reuse the enqueue handler's estimator so
|
|
110
|
+
// preflight and enqueue agree on the number. Caps at 500 docs per
|
|
111
|
+
// collection inside `estimateBatchCost`; bounded wall-clock.
|
|
112
|
+
let estimatedCostUsd;
|
|
113
|
+
try {
|
|
114
|
+
const resolved = resolveScope({}, config, bulkConfig);
|
|
115
|
+
const result = await estimateBatchCost(req.payload, config, resolved);
|
|
116
|
+
estimatedCostUsd = result.estimatedCostUsd;
|
|
117
|
+
} catch (err) {
|
|
118
|
+
req.payload.logger?.warn?.(`[ai-translate] preflight: estimate failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
119
|
+
estimatedCostUsd = undefined;
|
|
120
|
+
}
|
|
121
|
+
return Response.json({
|
|
122
|
+
scope: {
|
|
123
|
+
collections,
|
|
124
|
+
globals,
|
|
125
|
+
locales: targetLocales,
|
|
126
|
+
sourceLocale: config.sourceLocale
|
|
127
|
+
},
|
|
128
|
+
documents: {
|
|
129
|
+
total: totalDocs * Math.max(1, targetLocales.length),
|
|
130
|
+
perCollection
|
|
131
|
+
},
|
|
132
|
+
estimatedCostUsd,
|
|
133
|
+
dailySpend: {
|
|
134
|
+
spentUsd,
|
|
135
|
+
capUsd,
|
|
136
|
+
resetsAt
|
|
137
|
+
},
|
|
138
|
+
requireTotp,
|
|
139
|
+
totpEnrolled
|
|
140
|
+
});
|
|
141
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { PayloadHandler } from 'payload';
|
|
2
|
+
export interface BulkRetryFailedBody {
|
|
3
|
+
/**
|
|
4
|
+
* Failure codes to exclude from this retry (e.g. permanent classes
|
|
5
|
+
* a human reviewed and decided not to re-run). Optional.
|
|
6
|
+
*/
|
|
7
|
+
skipErrorCodes?: string[];
|
|
8
|
+
/**
|
|
9
|
+
* When set, only retry failed units in this collection or global. Used
|
|
10
|
+
* by the tree-grouped DrillDown's per-bucket "Retry N failed" CTA so
|
|
11
|
+
* the editor can retry one collection's subset without touching the
|
|
12
|
+
* others. Optional — omitted means "all failed units in the batch."
|
|
13
|
+
*/
|
|
14
|
+
collection?: string;
|
|
15
|
+
/**
|
|
16
|
+
* When set alongside `collection`, only retry failed units for THIS
|
|
17
|
+
* document. Used by the per-doc Retry button. Combined with
|
|
18
|
+
* `collection` rather than alone because a docId is only meaningful
|
|
19
|
+
* within a collection. Optional.
|
|
20
|
+
*/
|
|
21
|
+
documentId?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface BulkRetryFailedHandlerOptions {
|
|
24
|
+
batchesCollectionSlug?: string;
|
|
25
|
+
unitsCollectionSlug?: string;
|
|
26
|
+
workerTaskSlug?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* `POST /api/translation-hub/bulk-translate/:id/retry-failed`
|
|
30
|
+
*
|
|
31
|
+
* Re-enqueues all `failed` units for the batch back to `pending`.
|
|
32
|
+
* Refuses when the batch is non-terminal — there's no semantic way
|
|
33
|
+
* to "retry failed" while units are still running.
|
|
34
|
+
*
|
|
35
|
+
* Daily-cap is re-checked (F-SEC-TOTP-BYPASS — the cap applies to all
|
|
36
|
+
* translate paths, not just first-enqueue).
|
|
37
|
+
*/
|
|
38
|
+
export declare const getBulkTranslateRetryFailedHandler: (options?: BulkRetryFailedHandlerOptions) => PayloadHandler;
|