@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,391 @@
|
|
|
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 { createScopedLogger } from '../../lib/logger.js';
|
|
5
|
+
import { decodeCursor, encodeCursor, errorResponse, isEditorOrAdmin, unauthorizedResponse } from './_helpers.js';
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Handler
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
/**
|
|
10
|
+
* `GET /api/translation-hub/bulk-translate/:id/status`
|
|
11
|
+
*
|
|
12
|
+
* Returns batch metadata + aggregate counts by unit status + a
|
|
13
|
+
* paginated drill-down list of units (filterable by status).
|
|
14
|
+
*
|
|
15
|
+
* Query params:
|
|
16
|
+
* - `cursor` — opaque cursor returned by the previous page
|
|
17
|
+
* - `limit` — page size (default 20, max 100)
|
|
18
|
+
* - `status` — filter to one unit status (`failed` is the typical
|
|
19
|
+
* value for the failure drill-down UI)
|
|
20
|
+
*/ export const getBulkTranslateStatusHandler = (options = {})=>async (req)=>{
|
|
21
|
+
const batchesSlug = options.batchesCollectionSlug ?? DEFAULT_BULK_TRANSLATE_BATCHES_COLLECTION_SLUG;
|
|
22
|
+
const unitsSlug = options.unitsCollectionSlug ?? DEFAULT_BULK_TRANSLATE_UNITS_COLLECTION_SLUG;
|
|
23
|
+
if (!req.user) {
|
|
24
|
+
return unauthorizedResponse(req);
|
|
25
|
+
}
|
|
26
|
+
if (!isEditorOrAdmin(req.user)) {
|
|
27
|
+
return errorResponse('forbidden', "You don't have permission to view bulk-translation status. Contact an admin.", 403);
|
|
28
|
+
}
|
|
29
|
+
const batchId = extractBatchId(req.url ?? '');
|
|
30
|
+
if (!batchId) {
|
|
31
|
+
return errorResponse('invalid_batch_id', 'Batch ID could not be parsed from the request URL.', 400);
|
|
32
|
+
}
|
|
33
|
+
let batch;
|
|
34
|
+
try {
|
|
35
|
+
batch = await req.payload.findByID({
|
|
36
|
+
collection: batchesSlug,
|
|
37
|
+
id: batchId,
|
|
38
|
+
overrideAccess: true,
|
|
39
|
+
depth: 0
|
|
40
|
+
});
|
|
41
|
+
} catch (err) {
|
|
42
|
+
const log = createScopedLogger(req.payload, {
|
|
43
|
+
component: 'hub.status',
|
|
44
|
+
batchId
|
|
45
|
+
});
|
|
46
|
+
log.event('warn', 'hub.status.batch-read.failed', {
|
|
47
|
+
err,
|
|
48
|
+
endpoint: 'hub.status',
|
|
49
|
+
batchId
|
|
50
|
+
});
|
|
51
|
+
batch = undefined;
|
|
52
|
+
}
|
|
53
|
+
if (!batch) {
|
|
54
|
+
return errorResponse('not_found', 'This translation run no longer exists. Refresh the page.', 404);
|
|
55
|
+
}
|
|
56
|
+
const url = new URL(req.url ?? 'http://localhost');
|
|
57
|
+
const cursor = url.searchParams.get('cursor') ?? undefined;
|
|
58
|
+
const limitRaw = url.searchParams.get('limit');
|
|
59
|
+
const statusFilter = url.searchParams.get('status') ?? undefined;
|
|
60
|
+
const collectionFilter = url.searchParams.get('collection') ?? undefined;
|
|
61
|
+
let limit = limitRaw ? Number.parseInt(limitRaw, 10) : 20;
|
|
62
|
+
if (!Number.isFinite(limit) || limit <= 0) limit = 20;
|
|
63
|
+
if (limit > 100) limit = 100;
|
|
64
|
+
const offset = decodeCursor(cursor);
|
|
65
|
+
// Live counts from the units table — bypasses cached counters that
|
|
66
|
+
// drift on retries / cancels / admin SDK interventions. See
|
|
67
|
+
// `lib/batch-counts.ts` for the architecture note.
|
|
68
|
+
const liveCounts = await computeLiveBatchCount(req.payload, unitsSlug, batchId);
|
|
69
|
+
const counts = {
|
|
70
|
+
total: liveCounts.total,
|
|
71
|
+
pending: liveCounts.pending,
|
|
72
|
+
running: liveCounts.running,
|
|
73
|
+
completed: liveCounts.completed,
|
|
74
|
+
failed: liveCounts.failed,
|
|
75
|
+
skipped: liveCounts.skipped,
|
|
76
|
+
reverted: liveCounts.reverted
|
|
77
|
+
};
|
|
78
|
+
const countsByCollection = await aggregateUnitCountsByCollection(req.payload, unitsSlug, batchId);
|
|
79
|
+
const jobs = await fetchUnitPage(req.payload, unitsSlug, batchId, statusFilter, offset, limit, collectionFilter);
|
|
80
|
+
const nextCursor = jobs.length === limit ? encodeCursor(offset + limit) : null;
|
|
81
|
+
return Response.json({
|
|
82
|
+
data: {
|
|
83
|
+
batchId,
|
|
84
|
+
status: batch.status,
|
|
85
|
+
mode: batch.mode,
|
|
86
|
+
canaryLimit: batch.canaryLimit ?? null,
|
|
87
|
+
estimatedCostUsd: batch.estimatedCostUsd ?? null,
|
|
88
|
+
actualCostUsd: batch.actualCostUsd ?? 0,
|
|
89
|
+
counts,
|
|
90
|
+
countsByCollection,
|
|
91
|
+
jobs,
|
|
92
|
+
nextCursor,
|
|
93
|
+
snapshot: {
|
|
94
|
+
providerKey: batch.snapshotProviderKey ?? null,
|
|
95
|
+
modelId: batch.snapshotModelId ?? null,
|
|
96
|
+
sourceLocale: batch.snapshotSourceLocale ?? null
|
|
97
|
+
},
|
|
98
|
+
timestamps: {
|
|
99
|
+
enqueuedAt: batch.enqueuedAt ?? null,
|
|
100
|
+
startedAt: batch.startedAt ?? null,
|
|
101
|
+
completedAt: batch.completedAt ?? null,
|
|
102
|
+
cancelledAt: batch.cancelledAt ?? null,
|
|
103
|
+
revertedAt: batch.revertedAt ?? null
|
|
104
|
+
},
|
|
105
|
+
triggeredByUserId: batch.triggeredByUserId ?? null,
|
|
106
|
+
triggeredByEmail: batch.triggeredByEmail ?? null,
|
|
107
|
+
failures: batch.failures ?? null
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* Extracts the batch id from a URL of the shape
|
|
113
|
+
* `/api/translation-hub/bulk-translate/<id>/status`. Defensive across
|
|
114
|
+
* URL variants Payload may give us — we look for the segment between
|
|
115
|
+
* `bulk-translate` and `status`.
|
|
116
|
+
*/ export function extractBatchId(url) {
|
|
117
|
+
try {
|
|
118
|
+
const segments = new URL(url, 'http://placeholder').pathname.split('/').filter(Boolean);
|
|
119
|
+
const anchorIndex = segments.indexOf('bulk-translate');
|
|
120
|
+
if (anchorIndex === -1) return undefined;
|
|
121
|
+
const candidate = segments[anchorIndex + 1];
|
|
122
|
+
// Reject the literal endpoint paths (no id present).
|
|
123
|
+
if (!candidate || candidate === 'status' || candidate === 'enqueue') {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
return candidate;
|
|
127
|
+
} catch {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Per-collection aggregate counts + top-2 failure codes for a batch.
|
|
133
|
+
* Drives the tree DrillDown's bucket-header counts — without this, the
|
|
134
|
+
* UI would have to compute bucket totals from the paginated `jobs` page
|
|
135
|
+
* and the numbers would only reflect "how many of the loaded units are
|
|
136
|
+
* in this bucket", not the actual total.
|
|
137
|
+
*
|
|
138
|
+
* Strategy: one `payload.find` with `limit: 5000`, `depth: 0`, and
|
|
139
|
+
* `select` narrowed to (collection, documentId, status, failureCode).
|
|
140
|
+
* Aggregate in JS — provider-agnostic, no raw SQL. For batches above
|
|
141
|
+
* 5000 units we'd switch to drizzle GROUP BY but realistic batches
|
|
142
|
+
* stay well under that ceiling.
|
|
143
|
+
*/ async function aggregateUnitCountsByCollection(payload, unitsSlug, batchId) {
|
|
144
|
+
const out = {};
|
|
145
|
+
let page = 1;
|
|
146
|
+
// eslint-disable-next-line no-constant-condition
|
|
147
|
+
while(true){
|
|
148
|
+
const result = await payload.find({
|
|
149
|
+
collection: unitsSlug,
|
|
150
|
+
where: {
|
|
151
|
+
batchId: {
|
|
152
|
+
equals: batchId
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
page,
|
|
156
|
+
limit: 1000,
|
|
157
|
+
depth: 0,
|
|
158
|
+
// Everything the aggregation reads — and nothing else. The
|
|
159
|
+
// unselected `preRunSnapshot` jsonb is the heavy column (tens of
|
|
160
|
+
// KB per unit); without this projection every status-page load
|
|
161
|
+
// dragged the full snapshot payload of all units through
|
|
162
|
+
// Postgres TOAST + JSON.parse.
|
|
163
|
+
select: {
|
|
164
|
+
collection: true,
|
|
165
|
+
documentId: true,
|
|
166
|
+
status: true,
|
|
167
|
+
failureCode: true,
|
|
168
|
+
startedAt: true,
|
|
169
|
+
completedAt: true,
|
|
170
|
+
processingDurationMs: true
|
|
171
|
+
},
|
|
172
|
+
overrideAccess: true
|
|
173
|
+
});
|
|
174
|
+
for (const doc of result.docs){
|
|
175
|
+
const collection = String(doc.collection ?? '');
|
|
176
|
+
const documentId = String(doc.documentId ?? '');
|
|
177
|
+
const status = String(doc.status ?? '');
|
|
178
|
+
const failureCode = typeof doc.failureCode === 'string' ? doc.failureCode : null;
|
|
179
|
+
let bucket = out[collection];
|
|
180
|
+
if (!bucket) {
|
|
181
|
+
bucket = {
|
|
182
|
+
// For globals, the worker writes documentId === collection (the
|
|
183
|
+
// slug doubles as the id).
|
|
184
|
+
isGlobal: documentId === collection,
|
|
185
|
+
total: 0,
|
|
186
|
+
pending: 0,
|
|
187
|
+
running: 0,
|
|
188
|
+
succeeded: 0,
|
|
189
|
+
failed: 0,
|
|
190
|
+
skipped: 0,
|
|
191
|
+
reverted: 0,
|
|
192
|
+
_failureCodes: new Map(),
|
|
193
|
+
_docIds: new Set(),
|
|
194
|
+
_docIdsByPending: new Set(),
|
|
195
|
+
_docIdsByRunning: new Set(),
|
|
196
|
+
_docIdsBySucceeded: new Set(),
|
|
197
|
+
_docIdsByFailed: new Set(),
|
|
198
|
+
_docIdsBySkipped: new Set(),
|
|
199
|
+
_docIdsByReverted: new Set(),
|
|
200
|
+
_minStartedAtMs: null,
|
|
201
|
+
_maxCompletedAtMs: null,
|
|
202
|
+
_aiActiveMs: 0,
|
|
203
|
+
_aiActiveSeen: false
|
|
204
|
+
};
|
|
205
|
+
out[collection] = bucket;
|
|
206
|
+
}
|
|
207
|
+
bucket._docIds.add(documentId);
|
|
208
|
+
bucket.total += 1;
|
|
209
|
+
// Per-collection duration aggregates. min(startedAt) anchors the
|
|
210
|
+
// "when did we start translating this collection" moment;
|
|
211
|
+
// max(completedAt) is the last unit finish.
|
|
212
|
+
const startedAtRaw = doc.startedAt;
|
|
213
|
+
if (typeof startedAtRaw === 'string') {
|
|
214
|
+
const ms = new Date(startedAtRaw).getTime();
|
|
215
|
+
if (Number.isFinite(ms)) {
|
|
216
|
+
if (bucket._minStartedAtMs === null || ms < bucket._minStartedAtMs) {
|
|
217
|
+
bucket._minStartedAtMs = ms;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const completedAtRaw = doc.completedAt;
|
|
222
|
+
if (typeof completedAtRaw === 'string') {
|
|
223
|
+
const ms = new Date(completedAtRaw).getTime();
|
|
224
|
+
if (Number.isFinite(ms)) {
|
|
225
|
+
if (bucket._maxCompletedAtMs === null || ms > bucket._maxCompletedAtMs) {
|
|
226
|
+
bucket._maxCompletedAtMs = ms;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (typeof doc.processingDurationMs === 'number' && doc.processingDurationMs > 0) {
|
|
231
|
+
bucket._aiActiveMs += doc.processingDurationMs;
|
|
232
|
+
bucket._aiActiveSeen = true;
|
|
233
|
+
}
|
|
234
|
+
if (status === 'success') {
|
|
235
|
+
bucket.succeeded += 1;
|
|
236
|
+
bucket._docIdsBySucceeded.add(documentId);
|
|
237
|
+
} else if (status === 'failed') {
|
|
238
|
+
bucket.failed += 1;
|
|
239
|
+
bucket._docIdsByFailed.add(documentId);
|
|
240
|
+
if (failureCode) {
|
|
241
|
+
bucket._failureCodes.set(failureCode, (bucket._failureCodes.get(failureCode) ?? 0) + 1);
|
|
242
|
+
}
|
|
243
|
+
} else if (status === 'pending') {
|
|
244
|
+
bucket.pending += 1;
|
|
245
|
+
bucket._docIdsByPending.add(documentId);
|
|
246
|
+
} else if (status === 'running') {
|
|
247
|
+
bucket.running += 1;
|
|
248
|
+
bucket._docIdsByRunning.add(documentId);
|
|
249
|
+
} else if (status === 'skipped') {
|
|
250
|
+
bucket.skipped += 1;
|
|
251
|
+
bucket._docIdsBySkipped.add(documentId);
|
|
252
|
+
} else if (status === 'reverted') {
|
|
253
|
+
bucket.reverted += 1;
|
|
254
|
+
bucket._docIdsByReverted.add(documentId);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (!result.hasNextPage) break;
|
|
258
|
+
page += 1;
|
|
259
|
+
}
|
|
260
|
+
// Flatten + compute top-2 failure codes per bucket.
|
|
261
|
+
const flat = {};
|
|
262
|
+
for (const [collection, bucket] of Object.entries(out)){
|
|
263
|
+
const codeEntries = [
|
|
264
|
+
...bucket._failureCodes.entries()
|
|
265
|
+
];
|
|
266
|
+
const collectionSpanMs = bucket._minStartedAtMs !== null && bucket._maxCompletedAtMs !== null ? Math.max(0, bucket._maxCompletedAtMs - bucket._minStartedAtMs) : null;
|
|
267
|
+
const aiActiveMs = bucket._aiActiveSeen ? bucket._aiActiveMs : null;
|
|
268
|
+
// Queue wait is the gap between the editor-perceived wallclock and
|
|
269
|
+
// the actual AI compute time. Clamp to >=0 because aiActiveMs can
|
|
270
|
+
// exceed span when units ran in parallel across multiple workers.
|
|
271
|
+
const queueWaitMs = collectionSpanMs !== null && aiActiveMs !== null ? Math.max(0, collectionSpanMs - aiActiveMs) : null;
|
|
272
|
+
flat[collection] = {
|
|
273
|
+
isGlobal: bucket.isGlobal,
|
|
274
|
+
total: bucket.total,
|
|
275
|
+
pending: bucket.pending,
|
|
276
|
+
running: bucket.running,
|
|
277
|
+
succeeded: bucket.succeeded,
|
|
278
|
+
failed: bucket.failed,
|
|
279
|
+
skipped: bucket.skipped,
|
|
280
|
+
reverted: bucket.reverted,
|
|
281
|
+
docCountsByStatus: {
|
|
282
|
+
all: bucket._docIds.size,
|
|
283
|
+
pending: bucket._docIdsByPending.size,
|
|
284
|
+
running: bucket._docIdsByRunning.size,
|
|
285
|
+
completed: bucket._docIdsBySucceeded.size,
|
|
286
|
+
failed: bucket._docIdsByFailed.size,
|
|
287
|
+
skipped: bucket._docIdsBySkipped.size,
|
|
288
|
+
reverted: bucket._docIdsByReverted.size
|
|
289
|
+
},
|
|
290
|
+
totalDocs: bucket._docIds.size,
|
|
291
|
+
collectionSpanMs,
|
|
292
|
+
aiActiveMs,
|
|
293
|
+
queueWaitMs,
|
|
294
|
+
distinctFailureCodes: codeEntries.length,
|
|
295
|
+
topFailureCodes: codeEntries.sort((a, b)=>b[1] - a[1]).slice(0, 2).map(([code, count])=>({
|
|
296
|
+
code,
|
|
297
|
+
count
|
|
298
|
+
}))
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
return flat;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Type alias used by the aggregator above. Inlined here so the
|
|
305
|
+
* function signature stays self-documenting without a named export.
|
|
306
|
+
*/ function aggregateUnitCountsByCollectionShape() {
|
|
307
|
+
return {
|
|
308
|
+
isGlobal: false,
|
|
309
|
+
total: 0,
|
|
310
|
+
pending: 0,
|
|
311
|
+
running: 0,
|
|
312
|
+
succeeded: 0,
|
|
313
|
+
failed: 0,
|
|
314
|
+
skipped: 0,
|
|
315
|
+
reverted: 0,
|
|
316
|
+
docCountsByStatus: {
|
|
317
|
+
all: 0,
|
|
318
|
+
pending: 0,
|
|
319
|
+
running: 0,
|
|
320
|
+
completed: 0,
|
|
321
|
+
failed: 0,
|
|
322
|
+
skipped: 0,
|
|
323
|
+
reverted: 0
|
|
324
|
+
},
|
|
325
|
+
totalDocs: 0,
|
|
326
|
+
collectionSpanMs: null,
|
|
327
|
+
aiActiveMs: null,
|
|
328
|
+
queueWaitMs: null,
|
|
329
|
+
distinctFailureCodes: 0,
|
|
330
|
+
topFailureCodes: []
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
async function fetchUnitPage(payload, unitsSlug, batchId, statusFilter, offset, limit, collectionFilter) {
|
|
334
|
+
const andClauses = [
|
|
335
|
+
{
|
|
336
|
+
batchId: {
|
|
337
|
+
equals: batchId
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
];
|
|
341
|
+
if (statusFilter) {
|
|
342
|
+
// Map the public `completed` alias back onto storage's `success`.
|
|
343
|
+
const storageStatus = statusFilter === 'completed' ? 'success' : statusFilter;
|
|
344
|
+
andClauses.push({
|
|
345
|
+
status: {
|
|
346
|
+
equals: storageStatus
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
if (collectionFilter) {
|
|
351
|
+
// Used by the tree DrillDown's bucket-on-expand fetch to load just
|
|
352
|
+
// that collection's units rather than paging through every unit in
|
|
353
|
+
// the batch. The slug doubles as the global id, so this works for
|
|
354
|
+
// both collection and global buckets.
|
|
355
|
+
andClauses.push({
|
|
356
|
+
collection: {
|
|
357
|
+
equals: collectionFilter
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
// Payload doesn't expose an `offset`-based pagination today, so we
|
|
362
|
+
// compute the page number from offset+limit. This is correct so
|
|
363
|
+
// long as `limit` stays stable between requests — which is the
|
|
364
|
+
// contract of cursor-based pagination.
|
|
365
|
+
const page = Math.floor(offset / limit) + 1;
|
|
366
|
+
const result = await payload.find({
|
|
367
|
+
collection: unitsSlug,
|
|
368
|
+
where: {
|
|
369
|
+
and: andClauses
|
|
370
|
+
},
|
|
371
|
+
sort: 'createdAt',
|
|
372
|
+
page,
|
|
373
|
+
limit,
|
|
374
|
+
depth: 0,
|
|
375
|
+
overrideAccess: true
|
|
376
|
+
});
|
|
377
|
+
return result.docs.map((u)=>({
|
|
378
|
+
unitId: String(u.id),
|
|
379
|
+
collection: String(u.collection ?? ''),
|
|
380
|
+
documentId: String(u.documentId ?? ''),
|
|
381
|
+
locale: String(u.locale ?? ''),
|
|
382
|
+
status: String(u.status ?? ''),
|
|
383
|
+
attempts: Number(u.attempts ?? 0),
|
|
384
|
+
failureCode: typeof u.failureCode === 'string' ? u.failureCode : null,
|
|
385
|
+
failureMessage: typeof u.failureMessage === 'string' ? u.failureMessage : null,
|
|
386
|
+
costUsd: Number(u.costUsd ?? 0),
|
|
387
|
+
startedAt: typeof u.startedAt === 'string' ? u.startedAt : null,
|
|
388
|
+
completedAt: typeof u.completedAt === 'string' ? u.completedAt : null,
|
|
389
|
+
processingDurationMs: typeof u.processingDurationMs === 'number' ? u.processingDurationMs : null
|
|
390
|
+
}));
|
|
391
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { PayloadHandler } from 'payload';
|
|
2
|
+
export type UsageSummaryRange = {
|
|
3
|
+
kind: '7d' | '30d' | '90d';
|
|
4
|
+
since: string;
|
|
5
|
+
} | {
|
|
6
|
+
kind: 'all';
|
|
7
|
+
} | {
|
|
8
|
+
kind: 'custom';
|
|
9
|
+
from?: string;
|
|
10
|
+
to?: string;
|
|
11
|
+
};
|
|
12
|
+
export interface UsageSummaryTotals {
|
|
13
|
+
runs: number;
|
|
14
|
+
/**
|
|
15
|
+
* ROUND2-2 (v1.2.6, fix round): canonical "did this run write any
|
|
16
|
+
* fields" definition — `status = 'succeeded' AND fieldsTranslated > 0`.
|
|
17
|
+
* Counts only runs that actually called the LLM and persisted at
|
|
18
|
+
* least one field. Manual-edit-preserve-only runs (status =
|
|
19
|
+
* 'succeeded' but fieldsTranslated = 0) are NOT counted here; they
|
|
20
|
+
* surface as `preserved` instead.
|
|
21
|
+
*
|
|
22
|
+
* Before this fix, Hub Overview counted bare `status = 'succeeded'`
|
|
23
|
+
* (which includes preserved-only no-ops) while Audit & Cost counted
|
|
24
|
+
* `fieldsTranslated > 0`. Same KPI named the same way reported two
|
|
25
|
+
* different numbers — the editor-trust failure mode NEW-1 was filed
|
|
26
|
+
* to prevent.
|
|
27
|
+
*
|
|
28
|
+
* Invariant: `runs === succeeded + failed + preserved`.
|
|
29
|
+
*/
|
|
30
|
+
succeeded: number;
|
|
31
|
+
failed: number;
|
|
32
|
+
/**
|
|
33
|
+
* ROUND2-2: status = 'succeeded' AND fieldsTranslated <= 0. Manual-
|
|
34
|
+
* edit-guard preserves, hash-skip-only re-runs, etc. Surfaced as a
|
|
35
|
+
* separate count so editors can see "your translation didn't do
|
|
36
|
+
* nothing — it just had nothing new to write." Falls under "not
|
|
37
|
+
* failed" but isn't a write either.
|
|
38
|
+
*/
|
|
39
|
+
preserved: number;
|
|
40
|
+
inputTokens: number;
|
|
41
|
+
outputTokens: number;
|
|
42
|
+
costUsd: number;
|
|
43
|
+
totalMatching: number;
|
|
44
|
+
/**
|
|
45
|
+
* Always `false` from this endpoint — the SQL aggregation always
|
|
46
|
+
* covers every row matching the window. The flag is preserved on the
|
|
47
|
+
* response so the UI's existing truncation chip code can keep
|
|
48
|
+
* checking it without branching. Client-side aggregation is gone.
|
|
49
|
+
*/
|
|
50
|
+
truncated: boolean;
|
|
51
|
+
}
|
|
52
|
+
export interface UsageSummaryCollectionBucket {
|
|
53
|
+
slug: string;
|
|
54
|
+
kind: 'collection' | 'global';
|
|
55
|
+
runs: number;
|
|
56
|
+
tokens: number;
|
|
57
|
+
costUsd: number;
|
|
58
|
+
}
|
|
59
|
+
export interface UsageSummaryModelBucket {
|
|
60
|
+
/** Provider/model slug; may be the literal `'__unresolved__'` for
|
|
61
|
+
* rows that failed before model selection (see NEW-2). */
|
|
62
|
+
model: string;
|
|
63
|
+
runs: number;
|
|
64
|
+
tokens: number;
|
|
65
|
+
costUsd: number;
|
|
66
|
+
}
|
|
67
|
+
export interface UsageSummaryLocaleBucket {
|
|
68
|
+
locale: string;
|
|
69
|
+
runs: number;
|
|
70
|
+
failed: number;
|
|
71
|
+
}
|
|
72
|
+
export interface UsageSummarySampleRow {
|
|
73
|
+
id: number | string;
|
|
74
|
+
createdAt: string;
|
|
75
|
+
slug: string;
|
|
76
|
+
kind: 'collection' | 'global';
|
|
77
|
+
documentId?: string | null;
|
|
78
|
+
status: 'succeeded' | 'failed';
|
|
79
|
+
model?: string | null;
|
|
80
|
+
inputTokens: number;
|
|
81
|
+
outputTokens: number;
|
|
82
|
+
estimatedCostUsd?: number | null;
|
|
83
|
+
durationMs?: number | null;
|
|
84
|
+
error?: string | null;
|
|
85
|
+
failedCount: number;
|
|
86
|
+
succeededCount: number;
|
|
87
|
+
targetLocales?: Array<{
|
|
88
|
+
locale: string;
|
|
89
|
+
status?: 'succeeded' | 'failed' | null;
|
|
90
|
+
}> | null;
|
|
91
|
+
}
|
|
92
|
+
export interface UsageSummaryResponse {
|
|
93
|
+
range: UsageSummaryRange;
|
|
94
|
+
totals: UsageSummaryTotals;
|
|
95
|
+
byCollection: UsageSummaryCollectionBucket[];
|
|
96
|
+
byModel: UsageSummaryModelBucket[];
|
|
97
|
+
byLocale: UsageSummaryLocaleBucket[];
|
|
98
|
+
samples: UsageSummarySampleRow[];
|
|
99
|
+
}
|
|
100
|
+
export interface UsageSummaryHandlerOptions {
|
|
101
|
+
/** Override the usage collection slug. Defaults to `translation-usage`. */
|
|
102
|
+
usageCollectionSlug?: string;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Parse the `range`/`from`/`to` query params. Exported for testing —
|
|
106
|
+
* mirrors the StatusStrip/AuditPanel resolution rules so the UI and
|
|
107
|
+
* the endpoint agree on what "Last 7 days" means.
|
|
108
|
+
*/
|
|
109
|
+
export declare function parseRangeParams(params: URLSearchParams): {
|
|
110
|
+
range: UsageSummaryRange;
|
|
111
|
+
since: string | null;
|
|
112
|
+
until: string | null;
|
|
113
|
+
};
|
|
114
|
+
export declare const getTranslationHubUsageSummaryHandler: (options?: UsageSummaryHandlerOptions) => PayloadHandler;
|