@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,405 @@
|
|
|
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 { resolveTranslatableFields } from '../../lib/field-resolver.js';
|
|
4
|
+
import { createScopedLogger } from '../../lib/logger.js';
|
|
5
|
+
import { hashLocalizedSchema } from '../../lib/snapshot-select.js';
|
|
6
|
+
import { errorResponse, forbiddenOwnershipResponse, isEditorOrAdmin, ownsBatch, readJsonBody, unauthorizedResponse, verifyTotpCode, writeBulkAuditEntry } from './_helpers.js';
|
|
7
|
+
import { extractBatchId } from './status.js';
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Handler
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
const REVERTABLE_TERMINAL = new Set([
|
|
12
|
+
'success',
|
|
13
|
+
'partial'
|
|
14
|
+
]);
|
|
15
|
+
const IN_FLIGHT = new Set([
|
|
16
|
+
'queued',
|
|
17
|
+
'running',
|
|
18
|
+
'cancelling'
|
|
19
|
+
]);
|
|
20
|
+
/**
|
|
21
|
+
* `POST /api/translation-hub/bulk-translate/:id/revert`
|
|
22
|
+
*
|
|
23
|
+
* Restores the pre-run snapshot of every `success` unit in the batch.
|
|
24
|
+
* Decision #22 v2 / F-DA-REVERT: the snapshot is a nested-object
|
|
25
|
+
* payload directly passable to `payload.update({ data: snapshot })`.
|
|
26
|
+
*
|
|
27
|
+
* Refusal cases:
|
|
28
|
+
* - non-revertable batch status (must be `completed | partial`).
|
|
29
|
+
* - batch is currently in-flight (`queued | running | cancelling`).
|
|
30
|
+
* - batch's `completedAt` is more than `windowMs` ago.
|
|
31
|
+
* - TOTP required regardless of plugin config (Decision #33).
|
|
32
|
+
*
|
|
33
|
+
* Per-unit warnings (don't abort the run):
|
|
34
|
+
* - `schema_drift` — schema hash differs and `force !== true`.
|
|
35
|
+
* - `no_snapshot` — unit has no `preRunSnapshot` (older row).
|
|
36
|
+
* - `restore_failed` — `payload.update` threw.
|
|
37
|
+
* - `doc_deleted` — the source doc was deleted post-bulk; nothing
|
|
38
|
+
* to revert against.
|
|
39
|
+
*
|
|
40
|
+
* Writes are flagged via `context: { aiTranslateInternal: true,
|
|
41
|
+
* disableRevalidate: true }` so:
|
|
42
|
+
* - audit-log marks them `source: 'ai-translate'` (avoids confusing
|
|
43
|
+
* editor with 1000 "user updated" entries for a revert).
|
|
44
|
+
* - the plugin's allowlist-spread at translate.ts:483 + the
|
|
45
|
+
* consumer's revalidate hooks honor the disable flag — we'll
|
|
46
|
+
* fire a terminal revalidate sweep at the end.
|
|
47
|
+
*/ export const getBulkTranslateRevertHandler = (options = {})=>async (req)=>{
|
|
48
|
+
const batchesSlug = options.batchesCollectionSlug ?? DEFAULT_BULK_TRANSLATE_BATCHES_COLLECTION_SLUG;
|
|
49
|
+
const unitsSlug = options.unitsCollectionSlug ?? DEFAULT_BULK_TRANSLATE_UNITS_COLLECTION_SLUG;
|
|
50
|
+
const windowMs = options.windowMs ?? 24 * 60 * 60 * 1000;
|
|
51
|
+
const pageSize = options.pageSize ?? 100;
|
|
52
|
+
if (!req.user) {
|
|
53
|
+
return unauthorizedResponse(req);
|
|
54
|
+
}
|
|
55
|
+
if (!isEditorOrAdmin(req.user)) {
|
|
56
|
+
return errorResponse('forbidden', "You don't have permission to revert a bulk-translation batch. Contact an admin.", 403);
|
|
57
|
+
}
|
|
58
|
+
const batchId = extractBatchId(req.url ?? '');
|
|
59
|
+
if (!batchId) {
|
|
60
|
+
return errorResponse('invalid_batch_id', 'Batch ID could not be parsed from the request URL.', 400);
|
|
61
|
+
}
|
|
62
|
+
const parsed = await readJsonBody(req);
|
|
63
|
+
if (!parsed.ok) return parsed.res;
|
|
64
|
+
const body = parsed.body;
|
|
65
|
+
// ----- TOTP gate (Decision #33 — mandatory, not config-gated) -----
|
|
66
|
+
const totpResult = await verifyTotpCode(req.payload, req.user.id, body.totpCode, {
|
|
67
|
+
required: true,
|
|
68
|
+
// Revert is destructive — refuse if TOTP plugin is unavailable
|
|
69
|
+
// (better to fail loud than allow an unauthenticated revert).
|
|
70
|
+
allowSkipWhenUnavailable: false
|
|
71
|
+
});
|
|
72
|
+
if (!totpResult.ok) {
|
|
73
|
+
const status = totpResult.code === 'missing' ? 400 : 401;
|
|
74
|
+
return errorResponse('totp_' + totpResult.code, totpResult.message, status);
|
|
75
|
+
}
|
|
76
|
+
// ----- Load batch + state checks -----
|
|
77
|
+
let batch;
|
|
78
|
+
try {
|
|
79
|
+
batch = await req.payload.findByID({
|
|
80
|
+
collection: batchesSlug,
|
|
81
|
+
id: batchId,
|
|
82
|
+
overrideAccess: true,
|
|
83
|
+
depth: 0
|
|
84
|
+
});
|
|
85
|
+
} catch (err) {
|
|
86
|
+
const log = createScopedLogger(req.payload, {
|
|
87
|
+
component: 'hub.revert',
|
|
88
|
+
batchId
|
|
89
|
+
});
|
|
90
|
+
log.event('warn', 'hub.revert.batch-read.failed', {
|
|
91
|
+
err,
|
|
92
|
+
endpoint: 'hub.revert',
|
|
93
|
+
batchId
|
|
94
|
+
});
|
|
95
|
+
batch = undefined;
|
|
96
|
+
}
|
|
97
|
+
if (!batch) {
|
|
98
|
+
return errorResponse('not_found', 'This translation run no longer exists. Refresh the page.', 404);
|
|
99
|
+
}
|
|
100
|
+
// 1.2.8: editor can only revert a run they triggered. Admins pass.
|
|
101
|
+
// Revert is destructive so this gate is critical — without it an
|
|
102
|
+
// editor could roll back another editor's completed work.
|
|
103
|
+
if (!ownsBatch(req.user, batch)) {
|
|
104
|
+
return forbiddenOwnershipResponse();
|
|
105
|
+
}
|
|
106
|
+
if (IN_FLIGHT.has(batch.status)) {
|
|
107
|
+
return errorResponse('invalid_state', 'The run is still in progress. Cancel it first, then revert.', 409, {
|
|
108
|
+
status: batch.status
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
if (!REVERTABLE_TERMINAL.has(batch.status)) {
|
|
112
|
+
return errorResponse('invalid_state', 'Only completed or partial runs can be reverted — this run is in a different state.', 409, {
|
|
113
|
+
status: batch.status
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
if (!batch.completedAt) {
|
|
117
|
+
return errorResponse('invalid_state', "This run is missing a completion timestamp, so the system can't check if it's still revertable. Contact engineering.", 409);
|
|
118
|
+
}
|
|
119
|
+
const completedAtMs = new Date(batch.completedAt).getTime();
|
|
120
|
+
if (!Number.isFinite(completedAtMs)) {
|
|
121
|
+
return errorResponse('invalid_state', "This run's completion timestamp is unreadable. Contact engineering.", 409);
|
|
122
|
+
}
|
|
123
|
+
const ageMs = Date.now() - completedAtMs;
|
|
124
|
+
if (ageMs > windowMs) {
|
|
125
|
+
const hoursAgo = Math.floor(ageMs / 3_600_000);
|
|
126
|
+
const windowHours = Math.floor(windowMs / 3_600_000);
|
|
127
|
+
return errorResponse('window_expired', `The ${windowHours}-hour window to undo this run has passed (it ran ${hoursAgo} hours ago). Contact engineering if you need to restore earlier content.`, 409, {
|
|
128
|
+
hoursElapsed: String(hoursAgo),
|
|
129
|
+
hoursWindow: String(windowHours)
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
// ----- Compute current schema hashes per collection (Decision #34) -----
|
|
133
|
+
const currentSchemaHashByCollection = computeCurrentSchemaHashes(req.payload);
|
|
134
|
+
// ----- Paginate units and revert -----
|
|
135
|
+
const warnings = [];
|
|
136
|
+
let revertedUnits = 0;
|
|
137
|
+
let skippedUnits = 0;
|
|
138
|
+
const affectedCollections = new Set();
|
|
139
|
+
const affectedGlobals = new Set();
|
|
140
|
+
const pluginGlobals = new Set((req.payload.config?.globals ?? []).map((g)=>g.slug));
|
|
141
|
+
let page = 1;
|
|
142
|
+
// eslint-disable-next-line no-constant-condition
|
|
143
|
+
while(true){
|
|
144
|
+
const result = await req.payload.find({
|
|
145
|
+
collection: unitsSlug,
|
|
146
|
+
where: {
|
|
147
|
+
and: [
|
|
148
|
+
{
|
|
149
|
+
batchId: {
|
|
150
|
+
equals: batchId
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
status: {
|
|
155
|
+
equals: 'success'
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
]
|
|
159
|
+
},
|
|
160
|
+
page,
|
|
161
|
+
limit: pageSize,
|
|
162
|
+
depth: 0,
|
|
163
|
+
overrideAccess: true
|
|
164
|
+
});
|
|
165
|
+
const docs = result.docs;
|
|
166
|
+
for (const u of docs){
|
|
167
|
+
const unit = {
|
|
168
|
+
id: u.id,
|
|
169
|
+
collection: String(u.collection ?? ''),
|
|
170
|
+
documentId: String(u.documentId ?? ''),
|
|
171
|
+
locale: String(u.locale ?? ''),
|
|
172
|
+
preRunSnapshot: u.preRunSnapshot && typeof u.preRunSnapshot === 'object' ? u.preRunSnapshot : null,
|
|
173
|
+
snapshotLocale: typeof u.snapshotLocale === 'string' ? u.snapshotLocale : null,
|
|
174
|
+
schemaHash: typeof u.schemaHash === 'string' ? u.schemaHash : null
|
|
175
|
+
};
|
|
176
|
+
const outcome = await revertSingleUnit({
|
|
177
|
+
payload: req.payload,
|
|
178
|
+
unit,
|
|
179
|
+
unitsSlug,
|
|
180
|
+
currentSchemaHashByCollection,
|
|
181
|
+
force: body.force === true,
|
|
182
|
+
pluginGlobals
|
|
183
|
+
});
|
|
184
|
+
if (outcome.reverted) {
|
|
185
|
+
revertedUnits += 1;
|
|
186
|
+
if (pluginGlobals.has(unit.collection)) {
|
|
187
|
+
affectedGlobals.add(unit.collection);
|
|
188
|
+
} else {
|
|
189
|
+
affectedCollections.add(unit.collection);
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
skippedUnits += 1;
|
|
193
|
+
if (outcome.warning) warnings.push(outcome.warning);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (!result.hasNextPage) break;
|
|
197
|
+
page += 1;
|
|
198
|
+
}
|
|
199
|
+
// ----- Mark batch reverted -----
|
|
200
|
+
const revertedAt = new Date().toISOString();
|
|
201
|
+
try {
|
|
202
|
+
await req.payload.update({
|
|
203
|
+
collection: batchesSlug,
|
|
204
|
+
id: batchId,
|
|
205
|
+
data: {
|
|
206
|
+
status: 'reverted',
|
|
207
|
+
revertedAt,
|
|
208
|
+
revertedByUserId: String(req.user.id)
|
|
209
|
+
},
|
|
210
|
+
overrideAccess: true
|
|
211
|
+
});
|
|
212
|
+
} catch (err) {
|
|
213
|
+
req.payload.logger?.error?.(`[ai-translate] revert: failed to mark batch ${batchId} reverted: ${err instanceof Error ? err.message : String(err)}`);
|
|
214
|
+
}
|
|
215
|
+
// ----- Audit log (Decision #33) -----
|
|
216
|
+
const userEmail = typeof req.user.email === 'string' ? req.user.email : undefined;
|
|
217
|
+
await writeBulkAuditEntry(req.payload, {
|
|
218
|
+
userId: req.user.id,
|
|
219
|
+
userEmail,
|
|
220
|
+
action: 'bulk-translate.revert',
|
|
221
|
+
batchId,
|
|
222
|
+
metadata: {
|
|
223
|
+
revertedUnits,
|
|
224
|
+
skippedUnits,
|
|
225
|
+
warnings: warnings.length,
|
|
226
|
+
force: body.force === true,
|
|
227
|
+
affectedCollections: Array.from(affectedCollections),
|
|
228
|
+
affectedGlobals: Array.from(affectedGlobals)
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
// ----- Terminal revalidate sweep -----
|
|
232
|
+
await fireRevalidateSweep(req.payload, affectedCollections, affectedGlobals);
|
|
233
|
+
return Response.json({
|
|
234
|
+
data: {
|
|
235
|
+
batchId,
|
|
236
|
+
revertedUnits,
|
|
237
|
+
skippedUnits,
|
|
238
|
+
warnings,
|
|
239
|
+
revertedAt
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
};
|
|
243
|
+
function computeCurrentSchemaHashes(payload) {
|
|
244
|
+
const out = new Map();
|
|
245
|
+
for (const c of payload.config?.collections ?? []){
|
|
246
|
+
try {
|
|
247
|
+
const fields = resolveTranslatableFields(c.fields);
|
|
248
|
+
out.set(c.slug, hashLocalizedSchema(fields));
|
|
249
|
+
} catch {
|
|
250
|
+
// skip; the per-unit check will see a missing entry and warn
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
for (const g of payload.config?.globals ?? []){
|
|
254
|
+
try {
|
|
255
|
+
const fields = resolveTranslatableFields(g.fields);
|
|
256
|
+
out.set(g.slug, hashLocalizedSchema(fields));
|
|
257
|
+
} catch {
|
|
258
|
+
// skip
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return out;
|
|
262
|
+
}
|
|
263
|
+
export async function revertSingleUnit(params) {
|
|
264
|
+
const { payload, unit, unitsSlug, currentSchemaHashByCollection, force, pluginGlobals } = params;
|
|
265
|
+
if (!unit.preRunSnapshot || Object.keys(unit.preRunSnapshot).length === 0) {
|
|
266
|
+
return {
|
|
267
|
+
reverted: false,
|
|
268
|
+
warning: {
|
|
269
|
+
unitId: String(unit.id),
|
|
270
|
+
collection: unit.collection,
|
|
271
|
+
documentId: unit.documentId,
|
|
272
|
+
locale: unit.locale,
|
|
273
|
+
reason: 'no_snapshot',
|
|
274
|
+
message: 'No pre-run snapshot captured for this unit.'
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
// v1.2.12 — HARD gate, deliberately NOT overridable by `force`.
|
|
279
|
+
// Snapshots written before v1.2.12 were captured from the
|
|
280
|
+
// SOURCE-locale enumeration read and shared across every target
|
|
281
|
+
// locale of the doc; replaying one stamps source-language content
|
|
282
|
+
// over the translation it claims to restore (2026-06-11 prod
|
|
283
|
+
// incident: a batch revert turned every target locale English).
|
|
284
|
+
// Only a snapshot the worker captured FROM this unit's own locale
|
|
285
|
+
// may ever be written back to it.
|
|
286
|
+
if (unit.snapshotLocale !== unit.locale) {
|
|
287
|
+
return {
|
|
288
|
+
reverted: false,
|
|
289
|
+
warning: {
|
|
290
|
+
unitId: String(unit.id),
|
|
291
|
+
collection: unit.collection,
|
|
292
|
+
documentId: unit.documentId,
|
|
293
|
+
locale: unit.locale,
|
|
294
|
+
reason: 'snapshot_locale_mismatch',
|
|
295
|
+
message: unit.snapshotLocale === null ? 'Snapshot predates v1.2.12 and holds source-locale values — reverting would overwrite this translation with source-language content. Not revertable.' : `Snapshot holds "${unit.snapshotLocale}" values but this unit targets "${unit.locale}" — refusing to write cross-locale content.`
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
const currentHash = currentSchemaHashByCollection.get(unit.collection);
|
|
300
|
+
if (!force && currentHash && unit.schemaHash && currentHash !== unit.schemaHash) {
|
|
301
|
+
return {
|
|
302
|
+
reverted: false,
|
|
303
|
+
warning: {
|
|
304
|
+
unitId: String(unit.id),
|
|
305
|
+
collection: unit.collection,
|
|
306
|
+
documentId: unit.documentId,
|
|
307
|
+
locale: unit.locale,
|
|
308
|
+
reason: 'schema_drift',
|
|
309
|
+
message: 'Schema of localized translatable fields changed since snapshot. Re-run with force: true to override.'
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
// Restore the snapshot. We pass `context.aiTranslateInternal=true`
|
|
314
|
+
// to mark the write as machine-generated (audit-log will tag the
|
|
315
|
+
// entry `source: 'ai-translate'` rather than `user`) and
|
|
316
|
+
// `disableRevalidate=true` to suppress per-doc revalidation — we
|
|
317
|
+
// fire a single terminal sweep at the end of the revert.
|
|
318
|
+
try {
|
|
319
|
+
if (pluginGlobals.has(unit.collection)) {
|
|
320
|
+
await payload.updateGlobal({
|
|
321
|
+
slug: unit.collection,
|
|
322
|
+
locale: unit.locale,
|
|
323
|
+
data: unit.preRunSnapshot,
|
|
324
|
+
context: {
|
|
325
|
+
aiTranslateInternal: true,
|
|
326
|
+
disableRevalidate: true
|
|
327
|
+
},
|
|
328
|
+
overrideAccess: true
|
|
329
|
+
});
|
|
330
|
+
} else {
|
|
331
|
+
await payload.update({
|
|
332
|
+
collection: unit.collection,
|
|
333
|
+
id: unit.documentId,
|
|
334
|
+
locale: unit.locale,
|
|
335
|
+
data: unit.preRunSnapshot,
|
|
336
|
+
context: {
|
|
337
|
+
aiTranslateInternal: true,
|
|
338
|
+
disableRevalidate: true
|
|
339
|
+
},
|
|
340
|
+
overrideAccess: true
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
} catch (err) {
|
|
344
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
345
|
+
const isMissing = /not found/i.test(message);
|
|
346
|
+
return {
|
|
347
|
+
reverted: false,
|
|
348
|
+
warning: {
|
|
349
|
+
unitId: String(unit.id),
|
|
350
|
+
collection: unit.collection,
|
|
351
|
+
documentId: unit.documentId,
|
|
352
|
+
locale: unit.locale,
|
|
353
|
+
reason: isMissing ? 'doc_deleted' : 'restore_failed',
|
|
354
|
+
message
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
try {
|
|
359
|
+
await payload.update({
|
|
360
|
+
collection: unitsSlug,
|
|
361
|
+
id: unit.id,
|
|
362
|
+
data: {
|
|
363
|
+
status: 'reverted'
|
|
364
|
+
},
|
|
365
|
+
overrideAccess: true
|
|
366
|
+
});
|
|
367
|
+
} catch (err) {
|
|
368
|
+
// Unit-row update failure shouldn't drop us into a warning — the
|
|
369
|
+
// real-world write succeeded. Log and move on.
|
|
370
|
+
payload.logger?.warn?.(`[ai-translate] revert: failed to mark unit ${unit.id} reverted: ${err instanceof Error ? err.message : String(err)}`);
|
|
371
|
+
}
|
|
372
|
+
return {
|
|
373
|
+
reverted: true
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
async function fireRevalidateSweep(payload, collections, globals) {
|
|
377
|
+
// The plugin doesn't take a hard dependency on Next.js — instead,
|
|
378
|
+
// we attempt to dynamically import `next/cache` and call
|
|
379
|
+
// `revalidateTag` for each affected surface. This is best-effort:
|
|
380
|
+
// running in a non-Next environment (e.g. tests, Payload CLI) is
|
|
381
|
+
// simply a no-op.
|
|
382
|
+
let revalidateTag;
|
|
383
|
+
try {
|
|
384
|
+
const mod = await import('next/cache');
|
|
385
|
+
revalidateTag = mod.revalidateTag;
|
|
386
|
+
} catch {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
if (!revalidateTag) return;
|
|
390
|
+
for (const slug of collections){
|
|
391
|
+
try {
|
|
392
|
+
revalidateTag(`${slug}-sitemap`);
|
|
393
|
+
revalidateTag(slug);
|
|
394
|
+
} catch (err) {
|
|
395
|
+
payload.logger?.warn?.(`[ai-translate] revert: revalidateTag(${slug}) failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
for (const slug of globals){
|
|
399
|
+
try {
|
|
400
|
+
revalidateTag(slug);
|
|
401
|
+
} catch (err) {
|
|
402
|
+
payload.logger?.warn?.(`[ai-translate] revert: revalidateTag(${slug}) failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { PayloadHandler } from 'payload';
|
|
2
|
+
export type BatchJobSummary = {
|
|
3
|
+
unitId: string;
|
|
4
|
+
collection: string;
|
|
5
|
+
documentId: string;
|
|
6
|
+
locale: string;
|
|
7
|
+
status: string;
|
|
8
|
+
attempts: number;
|
|
9
|
+
failureCode: string | null;
|
|
10
|
+
failureMessage: string | null;
|
|
11
|
+
costUsd: number;
|
|
12
|
+
startedAt: string | null;
|
|
13
|
+
completedAt: string | null;
|
|
14
|
+
/**
|
|
15
|
+
* AI call time in milliseconds, summed across all batches for this
|
|
16
|
+
* unit's locale. Excludes token-bucket wait + rate-limit backoff —
|
|
17
|
+
* those happen before the SDK call and are not counted here.
|
|
18
|
+
* `null` on legacy rows where the column wasn't populated.
|
|
19
|
+
*/
|
|
20
|
+
processingDurationMs: number | null;
|
|
21
|
+
};
|
|
22
|
+
export interface BulkStatusHandlerOptions {
|
|
23
|
+
batchesCollectionSlug?: string;
|
|
24
|
+
unitsCollectionSlug?: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* `GET /api/translation-hub/bulk-translate/:id/status`
|
|
28
|
+
*
|
|
29
|
+
* Returns batch metadata + aggregate counts by unit status + a
|
|
30
|
+
* paginated drill-down list of units (filterable by status).
|
|
31
|
+
*
|
|
32
|
+
* Query params:
|
|
33
|
+
* - `cursor` — opaque cursor returned by the previous page
|
|
34
|
+
* - `limit` — page size (default 20, max 100)
|
|
35
|
+
* - `status` — filter to one unit status (`failed` is the typical
|
|
36
|
+
* value for the failure drill-down UI)
|
|
37
|
+
*/
|
|
38
|
+
export declare const getBulkTranslateStatusHandler: (options?: BulkStatusHandlerOptions) => PayloadHandler;
|
|
39
|
+
/**
|
|
40
|
+
* Extracts the batch id from a URL of the shape
|
|
41
|
+
* `/api/translation-hub/bulk-translate/<id>/status`. Defensive across
|
|
42
|
+
* URL variants Payload may give us — we look for the segment between
|
|
43
|
+
* `bulk-translate` and `status`.
|
|
44
|
+
*/
|
|
45
|
+
export declare function extractBatchId(url: string): string | undefined;
|