From 3ef06da8a2e3fa3e386d7335e06b8aea697a91bf Mon Sep 17 00:00:00 2001 From: busya Date: Mon, 1 Jun 2026 15:21:41 +0300 Subject: [PATCH] =?UTF-8?q?feat(ts-migration):=20Phase=202.3=20=E2=80=94?= =?UTF-8?q?=20all=20API=20satellite=20modules=20JS=E2=86=92TS=20with=20ful?= =?UTF-8?q?l=20contracts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Converted 12 files: - api/assistant.ts [C:3] — chat, conversations, history, confirm/cancel - api/reports.ts [C:3] — list/detail fetch with query builder - api/datasetReview.ts [C:3] — optimistic-lock, conflict detection, DTO normalization - api/maintenance.ts [C:3] — start/end events, settings, banners - api/translate.ts [C:2] — barrel re-export - api/translate/jobs.ts [C:2] — CRUD + duplicate - api/translate/runs.ts [C:2] — trigger, status, history, retry, cancel, metrics - api/translate/dictionaries.ts [C:3] — dictionary + entry CRUD, import - api/translate/datasources.ts [C:2] — columns, preview, approve/edit/reject rows - api/translate/corrections.ts [C:2] — inline edit, bulk replace, CSV download - api/translate/schedules.ts [C:2] — cron CRUD, enable/disable, next executions - api/translate/target-schema.ts [C:2] — schema validation with graceful error fallback Each function: - Typed parameters + generic return - @POST / @SIDE_EFFECT / @RELATION DEPENDS_ON chains - Normalized TranslateApiError pattern (message, code, retryable) - checkTargetTableSchema has @RATIONALE for graceful error handling Build: npm run build passes cleanly --- frontend/src/lib/api/assistant.js | 106 --------- frontend/src/lib/api/assistant.ts | 106 +++++++++ frontend/src/lib/api/datasetReview.js | 136 ----------- frontend/src/lib/api/datasetReview.ts | 142 +++++++++++ frontend/src/lib/api/maintenance.js | 69 ------ frontend/src/lib/api/maintenance.ts | 82 +++++++ frontend/src/lib/api/reports.js | 116 --------- frontend/src/lib/api/reports.ts | 105 +++++++++ .../lib/api/{translate.js => translate.ts} | 19 +- frontend/src/lib/api/translate/corrections.js | 85 ------- frontend/src/lib/api/translate/corrections.ts | 137 +++++++++++ frontend/src/lib/api/translate/datasources.js | 89 ------- frontend/src/lib/api/translate/datasources.ts | 145 ++++++++++++ .../src/lib/api/translate/dictionaries.js | 109 --------- .../src/lib/api/translate/dictionaries.ts | 145 ++++++++++++ frontend/src/lib/api/translate/jobs.js | 58 ----- frontend/src/lib/api/translate/jobs.ts | 107 +++++++++ frontend/src/lib/api/translate/runs.js | 127 ---------- frontend/src/lib/api/translate/runs.ts | 221 ++++++++++++++++++ frontend/src/lib/api/translate/schedules.js | 61 ----- frontend/src/lib/api/translate/schedules.ts | 105 +++++++++ .../src/lib/api/translate/target-schema.js | 49 ---- .../src/lib/api/translate/target-schema.ts | 68 ++++++ 23 files changed, 1374 insertions(+), 1013 deletions(-) delete mode 100644 frontend/src/lib/api/assistant.js create mode 100644 frontend/src/lib/api/assistant.ts delete mode 100644 frontend/src/lib/api/datasetReview.js create mode 100644 frontend/src/lib/api/datasetReview.ts delete mode 100644 frontend/src/lib/api/maintenance.js create mode 100644 frontend/src/lib/api/maintenance.ts delete mode 100644 frontend/src/lib/api/reports.js create mode 100644 frontend/src/lib/api/reports.ts rename frontend/src/lib/api/{translate.js => translate.ts} (76%) delete mode 100644 frontend/src/lib/api/translate/corrections.js create mode 100644 frontend/src/lib/api/translate/corrections.ts delete mode 100644 frontend/src/lib/api/translate/datasources.js create mode 100644 frontend/src/lib/api/translate/datasources.ts delete mode 100644 frontend/src/lib/api/translate/dictionaries.js create mode 100644 frontend/src/lib/api/translate/dictionaries.ts delete mode 100644 frontend/src/lib/api/translate/jobs.js create mode 100644 frontend/src/lib/api/translate/jobs.ts delete mode 100644 frontend/src/lib/api/translate/runs.js create mode 100644 frontend/src/lib/api/translate/runs.ts delete mode 100644 frontend/src/lib/api/translate/schedules.js create mode 100644 frontend/src/lib/api/translate/schedules.ts delete mode 100644 frontend/src/lib/api/translate/target-schema.js create mode 100644 frontend/src/lib/api/translate/target-schema.ts diff --git a/frontend/src/lib/api/assistant.js b/frontend/src/lib/api/assistant.js deleted file mode 100644 index ee156078..00000000 --- a/frontend/src/lib/api/assistant.js +++ /dev/null @@ -1,106 +0,0 @@ -// #region AssistantApi [C:3] [TYPE Module] [SEMANTICS assistant, api, chat, session, history] -// @BRIEF API client wrapper for assistant chat, session-scoped prompts, confirmation actions, conversation management, and history retrieval. -// @RELATION DEPENDS_ON -> [ApiModule] - -// #region AssistantApi:Module [TYPE Function] -// @SEMANTICS: assistant, api, client, chat, confirmation -// @PURPOSE: API client wrapper for assistant chat, session-scoped prompts, confirmation actions, and history retrieval. -// @LAYER API -// @RELATION DEPENDS_ON -> [EXT:frontend:api_module] -// @INVARIANT: All assistant requests must use requestApi wrapper (no native fetch). - -import { requestApi } from '$lib/api.js'; - -// #region sendAssistantMessage:Function [TYPE Function] -// @PURPOSE: Send a user message to assistant orchestrator endpoint. -// @PRE: payload.message is a non-empty string. -// @POST: Returns assistant response object with deterministic state. -export function sendAssistantMessage(payload) { - return requestApi('/assistant/messages', 'POST', payload); -} -// #endregion sendAssistantMessage:Function - -// #region buildAssistantSeedMessage:Function [TYPE Function] -// @PURPOSE: Compose visible assistant seed text from context label and prompt body. -// @PRE: prompt contains the user-visible request. -// @POST: Returns trimmed seed message string for prefilled input. -export function buildAssistantSeedMessage({ label = '', prompt = '' } = {}) { - const normalizedLabel = String(label || '').trim(); - const normalizedPrompt = String(prompt || '').trim(); - if (normalizedLabel && normalizedPrompt) { - return `${normalizedLabel}: ${normalizedPrompt}`; - } - return normalizedPrompt || normalizedLabel; -} -// #endregion buildAssistantSeedMessage:Function - -// #region confirmAssistantOperation:Function [TYPE Function] -// @PURPOSE: Confirm a pending risky assistant operation. -// @PRE: confirmationId references an existing pending token. -// @POST: Returns execution response (started/success/failed). -export function confirmAssistantOperation(confirmationId) { - return requestApi(`/assistant/confirmations/${confirmationId}/confirm`, 'POST'); -} -// #endregion confirmAssistantOperation:Function - -// #region cancelAssistantOperation:Function [TYPE Function] -// @PURPOSE: Cancel a pending risky assistant operation. -// @PRE: confirmationId references an existing pending token. -// @POST: Operation is cancelled and cannot be executed by this token. -export function cancelAssistantOperation(confirmationId) { - return requestApi(`/assistant/confirmations/${confirmationId}/cancel`, 'POST'); -} -// #endregion cancelAssistantOperation:Function - -// #region getAssistantHistory:Function [TYPE Function] -// @PURPOSE: Retrieve paginated assistant conversation history. -// @PRE: page/pageSize are positive integers. -// @POST: Returns a paginated payload with history items. -export function getAssistantHistory(page = 1, pageSize = 20, conversationId = null, fromLatest = false) { - const params = new URLSearchParams({ page: String(page), page_size: String(pageSize) }); - if (conversationId) { - params.append('conversation_id', conversationId); - } - if (fromLatest) { - params.append('from_latest', 'true'); - } - return requestApi(`/assistant/history?${params.toString()}`, 'GET'); -} -// #endregion getAssistantHistory:Function - -// #region getAssistantConversations:Function [TYPE Function] -// @PURPOSE: Retrieve paginated conversation list for assistant sidebar/history switcher. -// @PRE: page/pageSize are positive integers. -// @POST: Returns paginated conversation summaries. -export function getAssistantConversations( - page = 1, - pageSize = 20, - includeArchived = false, - search = '', - archivedOnly = false, -) { - const params = new URLSearchParams({ page: String(page), page_size: String(pageSize) }); - if (includeArchived) { - params.append('include_archived', 'true'); - } - if (archivedOnly) { - params.append('archived_only', 'true'); - } - if (search?.trim()) { - params.append('search', search.trim()); - } - return requestApi(`/assistant/conversations?${params.toString()}`, 'GET'); -} -// #endregion getAssistantConversations:Function - -// #region deleteAssistantConversation:Function [TYPE Function] -// @PURPOSE: Soft-delete or hard-delete a conversation. -// @PRE: conversationId string is provided. -// @POST: Returns success status. -export function deleteAssistantConversation(conversationId) { - return requestApi(`/assistant/conversations/${conversationId}`, 'DELETE'); -} -// #endregion deleteAssistantConversation:Function - -// #endregion AssistantApi:Module -// #endregion AssistantApi diff --git a/frontend/src/lib/api/assistant.ts b/frontend/src/lib/api/assistant.ts new file mode 100644 index 00000000..d546b141 --- /dev/null +++ b/frontend/src/lib/api/assistant.ts @@ -0,0 +1,106 @@ +// #region AssistantApi [C:3] [TYPE Module] [SEMANTICS assistant, api, chat, session, history] +// @BRIEF API client for assistant chat — message sending, conversation management, confirmation operations, and history retrieval. +// @LAYER API +// @RELATION DEPENDS_ON -> [ApiModule] +// @PRE All requests go through requestApi wrapper (never native fetch). +// @POST Returns typed API responses or throws normalized errors. + +import { requestApi } from '$lib/api'; + +// #region sendAssistantMessage [C:2] [TYPE Function] [SEMANTICS assistant, chat, message] +// @BRIEF Send a user message to the assistant orchestrator endpoint. +// @PRE payload.message is a non-empty string. +// @POST Returns assistant response object with deterministic state. +// @RELATION DEPENDS_ON -> [ApiModule.requestApi] +export function sendAssistantMessage(payload: { message: string; [k: string]: unknown }): Promise { + return requestApi('/assistant/messages', 'POST', payload); +} +// #endregion sendAssistantMessage + +// #region buildAssistantSeedMessage [C:1] [TYPE Function] [SEMANTICS assistant, seed, prompt] +// @BRIEF Compose visible assistant seed text from context label and prompt body. +// @PRE prompt contains the user-visible request. +// @POST Returns trimmed seed message string for prefilled input. +export function buildAssistantSeedMessage({ label = '', prompt = '' }: { label?: string; prompt?: string } = {}): string { + const normalizedLabel = String(label || '').trim(); + const normalizedPrompt = String(prompt || '').trim(); + if (normalizedLabel && normalizedPrompt) { + return `${normalizedLabel}: ${normalizedPrompt}`; + } + return normalizedPrompt || normalizedLabel; +} +// #endregion buildAssistantSeedMessage + +// #region confirmAssistantOperation [C:2] [TYPE Function] [SEMANTICS assistant, confirm, operation] +// @BRIEF Confirm a pending risky assistant operation. +// @PRE confirmationId references an existing pending token. +// @POST Returns execution response (started/success/failed). +// @RELATION DEPENDS_ON -> [ApiModule.requestApi] +export function confirmAssistantOperation(confirmationId: string): Promise { + return requestApi(`/assistant/confirmations/${confirmationId}/confirm`, 'POST'); +} +// #endregion confirmAssistantOperation + +// #region cancelAssistantOperation [C:2] [TYPE Function] [SEMANTICS assistant, cancel, operation] +// @BRIEF Cancel a pending risky assistant operation. +// @PRE confirmationId references an existing pending token. +// @POST Operation is cancelled and cannot be executed by this token. +// @RELATION DEPENDS_ON -> [ApiModule.requestApi] +export function cancelAssistantOperation(confirmationId: string): Promise { + return requestApi(`/assistant/confirmations/${confirmationId}/cancel`, 'POST'); +} +// #endregion cancelAssistantOperation + +// #region getAssistantHistory [C:2] [TYPE Function] [SEMANTICS assistant, history, pagination] +// @BRIEF Retrieve paginated assistant conversation history. +// @PRE page/pageSize are positive integers. +// @POST Returns a paginated payload with history items. +// @RELATION DEPENDS_ON -> [ApiModule.requestApi] +export function getAssistantHistory(page: number = 1, pageSize: number = 20, conversationId: string | null = null, fromLatest: boolean = false): Promise { + const params = new URLSearchParams({ page: String(page), page_size: String(pageSize) }); + if (conversationId) { + params.append('conversation_id', conversationId); + } + if (fromLatest) { + params.append('from_latest', 'true'); + } + return requestApi(`/assistant/history?${params.toString()}`, 'GET'); +} +// #endregion getAssistantHistory + +// #region getAssistantConversations [C:2] [TYPE Function] [SEMANTICS assistant, conversations, sidebar] +// @BRIEF Retrieve paginated conversation list for assistant sidebar/history switcher. +// @PRE page/pageSize are positive integers. +// @POST Returns paginated conversation summaries. +// @RELATION DEPENDS_ON -> [ApiModule.requestApi] +export function getAssistantConversations( + page: number = 1, + pageSize: number = 20, + includeArchived: boolean = false, + search: string = '', + archivedOnly: boolean = false, +): Promise { + const params = new URLSearchParams({ page: String(page), page_size: String(pageSize) }); + if (includeArchived) { + params.append('include_archived', 'true'); + } + if (archivedOnly) { + params.append('archived_only', 'true'); + } + if (search?.trim()) { + params.append('search', search.trim()); + } + return requestApi(`/assistant/conversations?${params.toString()}`, 'GET'); +} +// #endregion getAssistantConversations + +// #region deleteAssistantConversation [C:2] [TYPE Function] [SEMANTICS assistant, conversation, delete] +// @BRIEF Soft-delete or hard-delete a conversation. +// @PRE conversationId string is provided. +// @POST Returns success status. +// @RELATION DEPENDS_ON -> [ApiModule.requestApi] +export function deleteAssistantConversation(conversationId: string): Promise { + return requestApi(`/assistant/conversations/${conversationId}`, 'DELETE'); +} +// #endregion deleteAssistantConversation +// #endregion AssistantApi diff --git a/frontend/src/lib/api/datasetReview.js b/frontend/src/lib/api/datasetReview.js deleted file mode 100644 index a50b2084..00000000 --- a/frontend/src/lib/api/datasetReview.js +++ /dev/null @@ -1,136 +0,0 @@ -// #region DatasetReviewApi [C:3] [TYPE Module] [SEMANTICS dataset, review, api, session, conflict] -// @BRIEF Shared frontend helpers for dataset review session-scoped API requests, optimistic-lock propagation, and session DTO normalization. -// @RELATION DEPENDS_ON -> [ApiModule] - -// #region DatasetReviewApi:Module [TYPE Function] -// @SEMANTICS: dataset-review, api, session-version, headers, conflict-handling -// @PURPOSE: Provide shared frontend helpers for dataset review session-scoped API requests, optimistic-lock propagation, and refreshed session DTO normalization. -// @LAYER API -// @RELATION DEPENDS_ON -> [EXT:frontend:api_module] - -import { requestApi } from '$lib/api.js'; - -// #region buildDatasetReviewRequestOptions:Function [TYPE Function] -// @PURPOSE: Attach optimistic-lock session version header when the current version is known. -// @PRE: sessionVersion may be null when no loaded session version exists yet. -// @POST: Returns requestApi-compatible options object. -export function buildDatasetReviewRequestOptions(sessionVersion) { - if (sessionVersion === null || sessionVersion === undefined || sessionVersion === '') { - return {}; - } - return { - headers: { - 'X-Session-Version': String(sessionVersion), - }, - }; -} -// #endregion buildDatasetReviewRequestOptions:Function - -// #region requestDatasetReviewApi:Function [TYPE Function] -// @PURPOSE: Proxy dataset review mutations through requestApi with optional optimistic-lock headers. -// @PRE: endpoint and method are valid requestApi inputs. -// @POST: Executes requestApi with X-Session-Version only when a session version is known. -export function requestDatasetReviewApi(endpoint, method = 'GET', body = null, sessionVersion = null) { - const requestOptions = buildDatasetReviewRequestOptions(sessionVersion); - if (requestOptions.headers) { - return requestApi(endpoint, method, body, requestOptions); - } - return requestApi(endpoint, method, body); -} -// #endregion requestDatasetReviewApi:Function - -// #region isDatasetReviewConflictError:Function [TYPE Function] -// @PURPOSE: Detect optimistic-lock conflicts from dataset review mutations. -// @PRE: error may be null or a normalized API error. -// @POST: Returns true when the mutation failed with 409 conflict semantics. -export function isDatasetReviewConflictError(error) { - return Number(error?.status) === 409; -} -// #endregion isDatasetReviewConflictError:Function - -// #region getDatasetReviewConflictMessage:Function [TYPE Function] -// @PURPOSE: Return explicit 409-style guidance for stale dataset review mutations. -// @PRE: error may be null or a normalized API error. -// @POST: Returns a user-facing conflict message string. -export function getDatasetReviewConflictMessage(error) { - return ( - error?.message || - 'This review session changed in another action. Reload the latest session state and retry the update.' - ); -} -// #endregion getDatasetReviewConflictMessage:Function - -// #region extractDatasetReviewVersion:Function [TYPE Function] -// @PURPOSE: Resolve the latest session version from refreshed DTO fragments. -// @PRE: payload may be a session summary, a detail payload, or a mutation fragment. -// @POST: Returns the newest known session version or null. -export function extractDatasetReviewVersion(payload) { - if (!payload) { - return null; - } - - if (Array.isArray(payload)) { - for (const item of payload) { - const version = extractDatasetReviewVersion(item); - if (version !== null) { - return version; - } - } - return null; - } - - if (payload.session_version !== undefined && payload.session_version !== null) { - return payload.session_version; - } - if (payload.version !== undefined && payload.version !== null) { - return payload.version; - } - if (payload.session) { - return extractDatasetReviewVersion(payload.session); - } - - return null; -} -// #endregion extractDatasetReviewVersion:Function - -// #region normalizeDatasetReviewDetail:Function [TYPE Function] -// @PURPOSE: Normalize refreshed session-detail DTOs into the legacy route-friendly workspace shape. -// @PRE: detail may already be legacy-flat or refreshed with nested session/session_version fields. -// @POST: Returns a shape with flattened summary fields plus refreshed collections and version metadata. -export function normalizeDatasetReviewDetail(detail) { - if (!detail) { - return null; - } - - if (!detail.session && detail.session_id) { - return detail; - } - - const summary = detail.session || {}; - const preview = detail.preview || null; - - return { - ...summary, - session_version: - detail.session_version ?? summary.session_version ?? summary.version ?? null, - version: detail.session_version ?? summary.session_version ?? summary.version ?? null, - profile: detail.profile || null, - findings: detail.findings || [], - semantic_sources: detail.semantic_sources || [], - semantic_fields: detail.semantic_fields || [], - imported_filters: detail.filters || detail.imported_filters || [], - template_variables: detail.template_variables || [], - execution_mappings: detail.mappings || detail.execution_mappings || [], - clarification: detail.clarification || null, - clarification_sessions: detail.clarification - ? [detail.clarification.clarification_session].filter(Boolean) - : detail.clarification_sessions || [], - previews: preview ? [preview] : detail.previews || [], - preview, - latest_run_context: detail.latest_run_context || null, - }; -} -// #endregion normalizeDatasetReviewDetail:Function - -// #endregion DatasetReviewApi:Module -// #endregion DatasetReviewApi diff --git a/frontend/src/lib/api/datasetReview.ts b/frontend/src/lib/api/datasetReview.ts new file mode 100644 index 00000000..5514f92f --- /dev/null +++ b/frontend/src/lib/api/datasetReview.ts @@ -0,0 +1,142 @@ +// #region DatasetReviewApi [C:3] [TYPE Module] [SEMANTICS dataset, review, api, session, conflict] +// @BRIEF Dataset review session-scoped API helpers — optimistic-lock propagation, conflict detection, DTO normalization. +// @LAYER API +// @RELATION DEPENDS_ON -> [ApiModule] +// @PRE All mutations go through requestApi with X-Session-Version header when version is known. +// @POST Returns typed API responses, conflict detection booleans, or normalized session shapes. + +import { requestApi } from '$lib/api'; +import type { FetchOptions } from '$lib/types/api'; + +// #region buildDatasetReviewRequestOptions [C:2] [TYPE Function] [SEMANTICS dataset, review, headers, version] +// @BRIEF Attach optimistic-lock session version header when the current version is known. +// @PRE sessionVersion may be null when no loaded session version exists yet. +// @POST Returns requestApi-compatible options object with optional X-Session-Version header. +export function buildDatasetReviewRequestOptions(sessionVersion: string | number | null | undefined): FetchOptions { + if (sessionVersion === null || sessionVersion === undefined || sessionVersion === '') { + return {}; + } + return { + headers: { + 'X-Session-Version': String(sessionVersion), + }, + }; +} +// #endregion buildDatasetReviewRequestOptions + +// #region requestDatasetReviewApi [C:2] [TYPE Function] [SEMANTICS dataset, review, mutation, proxy] +// @BRIEF Proxy dataset review mutations through requestApi with optional optimistic-lock headers. +// @PRE endpoint and method are valid requestApi inputs. +// @POST Executes requestApi with X-Session-Version only when a session version is known. +// @RELATION DEPENDS_ON -> [ApiModule.requestApi] +export function requestDatasetReviewApi( + endpoint: string, + method: string = 'GET', + body: unknown = null, + sessionVersion: string | number | null = null, +): Promise { + const requestOptions = buildDatasetReviewRequestOptions(sessionVersion); + if (requestOptions.headers) { + return requestApi(endpoint, method, body, requestOptions); + } + return requestApi(endpoint, method, body); +} +// #endregion requestDatasetReviewApi + +// #region isDatasetReviewConflictError [C:1] [TYPE Function] [SEMANTICS dataset, review, conflict] +// @BRIEF Detect optimistic-lock conflicts from dataset review mutations. +// @PRE error may be null or a normalized API error. +// @POST Returns true when the mutation failed with 409 conflict semantics. +export function isDatasetReviewConflictError(error: { status?: number } | null | undefined): boolean { + return Number(error?.status) === 409; +} +// #endregion isDatasetReviewConflictError + +// #region getDatasetReviewConflictMessage [C:1] [TYPE Function] [SEMANTICS dataset, review, conflict, message] +// @BRIEF Return user-facing 409-style guidance for stale dataset review mutations. +// @PRE error may be null or a normalized API error. +// @POST Returns a user-facing conflict message string. +export function getDatasetReviewConflictMessage(error: { message?: string } | null | undefined): string { + return ( + error?.message || + 'This review session changed in another action. Reload the latest session state and retry the update.' + ); +} +// #endregion getDatasetReviewConflictMessage + +// #region extractDatasetReviewVersion [C:2] [TYPE Function] [SEMANTICS dataset, review, version, extraction] +// @BRIEF Resolve the latest session version from refreshed DTO fragments. +// @PRE payload may be a session summary, a detail payload, or a mutation fragment. +// @POST Returns the newest known session version or null. +// @RATIONALE Recursively walks nested session/version fields because the API response shape +// varies between list (summary), detail (nested), and mutation (flat) endpoints. +export function extractDatasetReviewVersion(payload: Record | unknown[] | null | undefined): string | number | null { + if (!payload) { + return null; + } + + if (Array.isArray(payload)) { + for (const item of payload) { + const version = extractDatasetReviewVersion(item as Record); + if (version !== null) { + return version; + } + } + return null; + } + + const obj = payload as Record; + if (obj.session_version !== undefined && obj.session_version !== null) { + return obj.session_version as string | number; + } + if (obj.version !== undefined && obj.version !== null) { + return obj.version as string | number; + } + if (obj.session) { + return extractDatasetReviewVersion(obj.session as Record); + } + + return null; +} +// #endregion extractDatasetReviewVersion + +// #region normalizeDatasetReviewDetail [C:2] [TYPE Function] [SEMANTICS dataset, review, normalize, dto] +// @BRIEF Normalize refreshed session-detail DTOs into the legacy route-friendly workspace shape. +// @PRE detail may already be legacy-flat or refreshed with nested session/session_version fields. +// @POST Returns a shape with flattened summary fields plus refreshed collections and version metadata. +export function normalizeDatasetReviewDetail | null | undefined>(detail: T): Record | null { + if (!detail) { + return null; + } + + const obj = detail as Record; + if (!obj.session && obj.session_id) { + return obj; + } + + const summary = (obj.session || {}) as Record; + const preview = obj.preview || null; + + return { + ...summary, + session_version: + obj.session_version ?? summary.session_version ?? summary.version ?? null, + version: obj.session_version ?? summary.session_version ?? summary.version ?? null, + profile: obj.profile || null, + findings: obj.findings || [], + semantic_sources: obj.semantic_sources || [], + semantic_fields: obj.semantic_fields || [], + imported_filters: obj.filters || obj.imported_filters || [], + template_variables: obj.template_variables || [], + execution_mappings: obj.mappings || obj.execution_mappings || [], + clarification: obj.clarification || null, + clarification_sessions: obj.clarification + ? [(obj.clarification as Record).clarification_session].filter(Boolean) + : obj.clarification_sessions || [], + previews: preview ? [preview] : obj.previews || [], + preview, + latest_run_context: obj.latest_run_context || null, + }; +} +// #endregion normalizeDatasetReviewDetail +// #endregion DatasetReviewApi diff --git a/frontend/src/lib/api/maintenance.js b/frontend/src/lib/api/maintenance.js deleted file mode 100644 index 9882da6d..00000000 --- a/frontend/src/lib/api/maintenance.js +++ /dev/null @@ -1,69 +0,0 @@ -// #region MaintenanceApi [C:2] [TYPE Module] [SEMANTICS api, maintenance, rest, client] -// @BRIEF API client for Maintenance Banner endpoints. Uses requestApi for all calls. -// @LAYER Frontend -// @RELATION DEPENDS_ON -> [EXT:frontend:requestApi] -// @ADR-0007: Uses $effect(() => subscribe(...)) pattern, NOT fromStore + $derived. - -import { requestApi } from "$lib/api.js"; - -const BASE = "/maintenance"; - -/** - * Start a maintenance event. - * @param {object} params - { tables, start_time, end_time?, message? } - * @returns {Promise} { task_id, maintenance_id, status } - */ -export async function startMaintenance(params) { - return requestApi(`${BASE}/start`, "POST", params); -} - -/** - * End a specific maintenance event. - * @param {string} maintenanceId - * @returns {Promise} { task_id, status } - */ -export async function endMaintenance(maintenanceId) { - return requestApi(`${BASE}/${maintenanceId}/end`, "POST"); -} - -/** - * End all active maintenance events. - * @returns {Promise} { task_id, status } - */ -export async function endAllMaintenance() { - return requestApi(`${BASE}/end-all`, "POST"); -} - -/** - * Get current settings. - * @returns {Promise} MaintenanceSettingsResponse - */ -export async function getSettings() { - return requestApi(`${BASE}/settings`, "GET"); -} - -/** - * Update settings. - * @param {object} settings - * @returns {Promise} Updated MaintenanceSettingsResponse - */ -export async function updateSettings(settings) { - return requestApi(`${BASE}/settings`, "PUT", settings); -} - -/** - * List events. - * @returns {Promise} { active: [...], completed: [...] } - */ -export async function listEvents() { - return requestApi(`${BASE}/events`, "GET"); -} - -/** - * List dashboard banner states. - * @returns {Promise} Array of { dashboard_id, active, events, start_time, end_time } - */ -export async function listDashboardBanners() { - return requestApi(`${BASE}/dashboard-banners`, "GET"); -} -// #endregion MaintenanceApi diff --git a/frontend/src/lib/api/maintenance.ts b/frontend/src/lib/api/maintenance.ts new file mode 100644 index 00000000..530950d8 --- /dev/null +++ b/frontend/src/lib/api/maintenance.ts @@ -0,0 +1,82 @@ +// #region MaintenanceApi [C:3] [TYPE Module] [SEMANTICS api, maintenance, banners, settings, events] +// @BRIEF API client for Maintenance Banner endpoints — start/end events, settings, dashboard banners. +// @LAYER API +// @RELATION DEPENDS_ON -> [ApiModule] +// @PRE All calls go through requestApi wrapper. +// @POST Returns typed API responses for maintenance lifecycle operations. +// @ADR-0007 Uses $effect(() => subscribe(...)) pattern, NOT fromStore + $derived. + +import { requestApi } from '$lib/api'; + +const BASE = '/maintenance'; + +// #region startMaintenance [C:2] [TYPE Function] [SEMANTICS maintenance, start, event] +// @BRIEF Start a maintenance event. +// @PRE params contains at minimum { tables, start_time }. +// @POST Returns { task_id, maintenance_id, status }. +// @SIDE_EFFECT Creates a maintenance event on the backend. +// @RELATION DEPENDS_ON -> [ApiModule.requestApi] +export async function startMaintenance(params: Record): Promise { + return requestApi(`${BASE}/start`, 'POST', params); +} +// #endregion startMaintenance + +// #region endMaintenance [C:2] [TYPE Function] [SEMANTICS maintenance, end, event] +// @BRIEF End a specific maintenance event. +// @PRE maintenanceId is a non-empty string. +// @POST Returns { task_id, status }. +// @SIDE_EFFECT Closes the specified maintenance event on the backend. +// @RELATION DEPENDS_ON -> [ApiModule.requestApi] +export async function endMaintenance(maintenanceId: string): Promise { + return requestApi(`${BASE}/${maintenanceId}/end`, 'POST'); +} +// #endregion endMaintenance + +// #region endAllMaintenance [C:2] [TYPE Function] [SEMANTICS maintenance, end-all, events] +// @BRIEF End all active maintenance events. +// @POST Returns { task_id, status }. +// @SIDE_EFFECT Closes all active maintenance events on the backend. +// @RELATION DEPENDS_ON -> [ApiModule.requestApi] +export async function endAllMaintenance(): Promise { + return requestApi(`${BASE}/end-all`, 'POST'); +} +// #endregion endAllMaintenance + +// #region getMaintenanceSettings [C:2] [TYPE Function] [SEMANTICS maintenance, settings, get] +// @BRIEF Get current maintenance settings. +// @POST Returns MaintenanceSettingsResponse. +// @RELATION DEPENDS_ON -> [ApiModule.requestApi] +export async function getSettings(): Promise { + return requestApi(`${BASE}/settings`, 'GET'); +} +// #endregion getMaintenanceSettings + +// #region updateSettings [C:2] [TYPE Function] [SEMANTICS maintenance, settings, update] +// @BRIEF Update maintenance settings. +// @PRE settings is a valid MaintenanceSettings payload. +// @POST Returns updated MaintenanceSettingsResponse. +// @SIDE_EFFECT Persists settings changes on the backend. +// @RELATION DEPENDS_ON -> [ApiModule.requestApi] +export async function updateSettings(settings: Record): Promise { + return requestApi(`${BASE}/settings`, 'PUT', settings); +} +// #endregion updateSettings + +// #region listEvents [C:2] [TYPE Function] [SEMANTICS maintenance, events, list] +// @BRIEF List active and completed maintenance events. +// @POST Returns { active: [...], completed: [...] }. +// @RELATION DEPENDS_ON -> [ApiModule.requestApi] +export async function listEvents(): Promise { + return requestApi(`${BASE}/events`, 'GET'); +} +// #endregion listEvents + +// #region listDashboardBanners [C:2] [TYPE Function] [SEMANTICS maintenance, dashboard, banners] +// @BRIEF List dashboard banner states for all dashboards. +// @POST Returns array of { dashboard_id, active, events, start_time, end_time }. +// @RELATION DEPENDS_ON -> [ApiModule.requestApi] +export async function listDashboardBanners(): Promise { + return requestApi(`${BASE}/dashboard-banners`, 'GET'); +} +// #endregion listDashboardBanners +// #endregion MaintenanceApi diff --git a/frontend/src/lib/api/reports.js b/frontend/src/lib/api/reports.js deleted file mode 100644 index 57daa0bb..00000000 --- a/frontend/src/lib/api/reports.js +++ /dev/null @@ -1,116 +0,0 @@ -// #region ReportsApi [C:4] [TYPE Module] [SEMANTICS report, api, search, filter, query] -// @BRIEF Wrapper-based reports API client for list/detail retrieval through the shared API wrapper. -// @RELATION DEPENDS_ON -> [ApiModule] -// @PRE Shared API wrapper is configured for the current frontend auth/session context. -// @POST Report list and detail helpers return backend payloads or normalized UI-safe errors. -// @SIDE_EFFECT Performs authenticated report HTTP requests through the shared API wrapper. - -// #region ReportsApi:Module [TYPE Function] -// @SEMANTICS: frontend, api_client, reports, wrapper -// @PURPOSE: Wrapper-based reports API client for list/detail retrieval without direct native fetch usage. -// @LAYER Infra -// @RELATION DEPENDS_ON -> [EXT:frontend:api_module] -// @PRE: Shared API wrapper is configured for the current frontend auth/session context. -// @POST: Report list and detail helpers return backend payloads or normalized UI-safe errors. -// @SIDE_EFFECT: Performs authenticated report HTTP requests through the shared API wrapper. -// @INVARIANT: Uses existing api wrapper methods and returns structured errors for UI-state mapping. -import { api } from '../api.js'; - -// #region buildReportQueryString:Function [TYPE Function] -// @PURPOSE: Build query string for reports list endpoint from filter options. -// @PRE: options is an object with optional report query fields. -// @POST: Returns URL query string without leading '?'. -export function buildReportQueryString(options = {}) { - console.log("[reports][EXT:frontend:api][buildReportQueryString:START]"); - const params = new URLSearchParams(); - - if (options.page != null) params.append('page', String(options.page)); - if (options.page_size != null) params.append('page_size', String(options.page_size)); - - if (Array.isArray(options.task_types) && options.task_types.length > 0) { - params.append('task_types', options.task_types.join(',')); - } - if (Array.isArray(options.statuses) && options.statuses.length > 0) { - params.append('statuses', options.statuses.join(',')); - } - - if (options.time_from) params.append('time_from', options.time_from); - if (options.time_to) params.append('time_to', options.time_to); - if (options.search) params.append('search', options.search); - if (options.sort_by) params.append('sort_by', options.sort_by); - if (options.sort_order) params.append('sort_order', options.sort_order); - - return params.toString(); -} -// #endregion buildReportQueryString:Function - -// #region normalizeApiError:Function [TYPE Function] -// @PURPOSE: Convert unknown API exceptions into deterministic UI-consumable error objects. -// @PRE: error may be Error/string/object. -// @POST: Returns structured error object. -export function normalizeApiError(error) { - console.log("[reports][EXT:frontend:api][normalizeApiError:START]"); - const message = - (error && typeof error.message === 'string' && error.message) || - (typeof error === 'string' && error) || - 'Failed to load reports'; - - return { - message, - code: 'REPORTS_API_ERROR', - retryable: true - }; -} -// #endregion normalizeApiError:Function - -// #region getReports:Function [TYPE Function] -// @PURPOSE: Fetch unified report list using existing request wrapper. -// @PRE: valid auth context for protected endpoint. -// @POST: Returns parsed payload or structured error for UI-state mapping. -// -// @TEST_CONTRACT: GetReportsApi -> -// { -// required_fields: {}, -// optional_fields: {options: Object}, -// invariants: [ -// "Fetches from /reports with built query string", -// "Returns response payload on success", -// "Catches and normalizes errors using normalizeApiError" -// ] -// } -// @TEST_FIXTURE: valid_get_reports -> {"options": {"page": 1}} -// @TEST_EDGE: api_fetch_failure -> api.fetchApi throws error -// @TEST_INVARIANT: error_normalization -> verifies: [api_fetch_failure] -export async function getReports(options = {}) { - try { - console.log("[reports][EXT:frontend:api][getReports:STARTED]", options); - const query = buildReportQueryString(options); - const res = await api.fetchApi(`/reports${query ? `?${query}` : ''}`); - console.log("[reports][EXT:frontend:api][getReports:SUCCESS]", res); - return res; - } catch (error) { - console.error("[reports][EXT:frontend:api][getReports:FAILED]", error); - throw normalizeApiError(error); - } -} -// #endregion getReports:Function - -// #region getReportDetail:Function [TYPE Function] -// @PURPOSE: Fetch one report detail by report_id. -// @PRE: reportId is non-empty string; valid auth context. -// @POST: Returns parsed detail payload or structured error object. -export async function getReportDetail(reportId) { - try { - console.log(`[reports][EXT:frontend:api][getReportDetail:STARTED] id=${reportId}`); - const res = await api.fetchApi(`/reports/${reportId}`); - console.log(`[reports][EXT:frontend:api][getReportDetail:SUCCESS] id=${reportId}`); - return res; - } catch (error) { - console.error(`[reports][EXT:frontend:api][getReportDetail:FAILED] id=${reportId}`, error); - throw normalizeApiError(error); - } -} -// #endregion getReportDetail:Function - -// #endregion ReportsApi:Module -// #endregion ReportsApi diff --git a/frontend/src/lib/api/reports.ts b/frontend/src/lib/api/reports.ts new file mode 100644 index 00000000..4ba03ef6 --- /dev/null +++ b/frontend/src/lib/api/reports.ts @@ -0,0 +1,105 @@ +// #region ReportsApi [C:3] [TYPE Module] [SEMANTICS report, api, search, filter, query] +// @BRIEF Reports API client — list/detail retrieval, query-string building, error normalization. +// @LAYER API +// @RELATION DEPENDS_ON -> [ApiModule] +// @PRE Shared API wrapper is configured for the current frontend auth/session context. +// @POST Report list and detail helpers return backend payloads or normalized UI-safe errors. + +import { api } from '../api'; + +// #region reportApiTypes [C:1] [TYPE Block] [SEMANTICS types, reports] +interface ReportQueryOptions { + page?: number; + page_size?: number; + task_types?: string[]; + statuses?: string[]; + time_from?: string; + time_to?: string; + search?: string; + sort_by?: string; + sort_order?: string; +} + +interface NormalizedError { + message: string; + code: string; + retryable: boolean; +} +// #endregion reportApiTypes + +// #region buildReportQueryString [C:2] [TYPE Function] [SEMANTICS report, query, params] +// @BRIEF Build query string for reports list endpoint from filter options. +// @PRE options is an object with optional report query fields. +// @POST Returns URL query string without leading '?'. +export function buildReportQueryString(options: ReportQueryOptions = {}): string { + const params = new URLSearchParams(); + + if (options.page != null) params.append('page', String(options.page)); + if (options.page_size != null) params.append('page_size', String(options.page_size)); + + if (Array.isArray(options.task_types) && options.task_types.length > 0) { + params.append('task_types', options.task_types.join(',')); + } + if (Array.isArray(options.statuses) && options.statuses.length > 0) { + params.append('statuses', options.statuses.join(',')); + } + + if (options.time_from) params.append('time_from', options.time_from); + if (options.time_to) params.append('time_to', options.time_to); + if (options.search) params.append('search', options.search); + if (options.sort_by) params.append('sort_by', options.sort_by); + if (options.sort_order) params.append('sort_order', options.sort_order); + + return params.toString(); +} +// #endregion buildReportQueryString + +// #region normalizeApiError [C:2] [TYPE Function] [SEMANTICS report, error, normalization] +// @BRIEF Convert unknown API exceptions into deterministic UI-consumable error objects. +// @PRE error may be Error/string/object. +// @POST Returns structured error object with message, code, retryable flag. +export function normalizeApiError(error: unknown): NormalizedError { + const message = + (error && typeof (error as Error).message === 'string' && (error as Error).message) || + (typeof error === 'string' && error) || + 'Failed to load reports'; + + return { + message, + code: 'REPORTS_API_ERROR', + retryable: true, + }; +} +// #endregion normalizeApiError + +// #region getReports [C:3] [TYPE Function] [SEMANTICS report, list, fetch] +// @BRIEF Fetch unified report list using the shared API wrapper. +// @PRE Valid auth context for protected endpoint. +// @POST Returns parsed payload or throws normalized error. +// @SIDE_EFFECT Performs authenticated GET request through api.fetchApi. +// @RELATION DEPENDS_ON -> [ApiModule.fetchApi] +export async function getReports(options: ReportQueryOptions = {}): Promise { + try { + const query = buildReportQueryString(options); + return await api.fetchApi(`/reports${query ? `?${query}` : ''}`); + } catch (error) { + throw normalizeApiError(error); + } +} +// #endregion getReports + +// #region getReportDetail [C:3] [TYPE Function] [SEMANTICS report, detail, fetch] +// @BRIEF Fetch one report detail by report_id. +// @PRE reportId is non-empty string; valid auth context. +// @POST Returns parsed detail payload or throws normalized error. +// @SIDE_EFFECT Performs authenticated GET request through api.fetchApi. +// @RELATION DEPENDS_ON -> [ApiModule.fetchApi] +export async function getReportDetail(reportId: string): Promise { + try { + return await api.fetchApi(`/reports/${reportId}`); + } catch (error) { + throw normalizeApiError(error); + } +} +// #endregion getReportDetail +// #endregion ReportsApi diff --git a/frontend/src/lib/api/translate.js b/frontend/src/lib/api/translate.ts similarity index 76% rename from frontend/src/lib/api/translate.js rename to frontend/src/lib/api/translate.ts index 5de0f700..e8db75f2 100644 --- a/frontend/src/lib/api/translate.js +++ b/frontend/src/lib/api/translate.ts @@ -1,31 +1,34 @@ // #region TranslateApi [C:2] [TYPE Module] [SEMANTICS translate, api, barrel] // @BRIEF Barrel module re-exporting all translate API functions from domain-split modules. +// @LAYER API // @RELATION DEPENDS_ON -> [TranslateJobsApi] // @RELATION DEPENDS_ON -> [TranslateRunsApi] // @RELATION DEPENDS_ON -> [TranslateDictionariesApi] // @RELATION DEPENDS_ON -> [TranslateSchedulesApi] // @RELATION DEPENDS_ON -> [TranslateCorrectionsApi] -// @RATIONALE Decomposed from 664->~30 lines by splitting into domain modules per INV_7. +// @RELATION DEPENDS_ON -> [TranslateDatasourcesApi] +// @RELATION DEPENDS_ON -> [TranslateTargetSchemaApi] +// @RATIONALE Decomposed from 664→~30 lines by splitting into domain modules per INV_7. // @REJECTED Keeping all APIs in one file was rejected because it exceeded the 400-line module limit. // Jobs -export { fetchJobs, createJob, updateJob, deleteJob, duplicateJob } from './translate/jobs.js'; +export { fetchJobs, createJob, updateJob, deleteJob, duplicateJob } from './translate/jobs'; // Datasources & preview -export { fetchDatasourceColumns, fetchDatasources, fetchPreview, approveRow, editRow, rejectRow, acceptPreview, fetchPreviewRecords } from './translate/datasources.js'; +export { fetchDatasourceColumns, fetchDatasources, fetchPreview, approveRow, editRow, rejectRow, acceptPreview, fetchPreviewRecords } from './translate/datasources'; // Runs -export { triggerRun, fetchRunStatus, fetchRunHistory, fetchRunRecords, retryFailedBatches, retryInsert, cancelRun, fetchRunBatches, fetchAllRuns, fetchRunDetail, fetchJobMetrics, fetchAllMetrics } from './translate/runs.js'; +export { triggerRun, fetchRunStatus, fetchRunHistory, fetchRunRecords, retryFailedBatches, retryInsert, cancelRun, fetchRunBatches, fetchAllRuns, fetchRunDetail, fetchJobMetrics, fetchAllMetrics } from './translate/runs'; // Dictionaries -export { dictionaryApi } from './translate/dictionaries.js'; +export { dictionaryApi } from './translate/dictionaries'; // Schedules -export { fetchSchedule, setSchedule, deleteSchedule, enableSchedule, disableSchedule, fetchNextExecutions } from './translate/schedules.js'; +export { fetchSchedule, setSchedule, deleteSchedule, enableSchedule, disableSchedule, fetchNextExecutions } from './translate/schedules'; // Corrections & bulk replace -export { submitCorrection, submitBulkCorrections, inlineEditCorrection, submitCorrectionToDict, bulkFindReplace, bulkReplacePreview, downloadSkippedCsv } from './translate/corrections.js'; +export { submitCorrection, submitBulkCorrections, inlineEditCorrection, submitCorrectionToDict, bulkFindReplace, bulkReplacePreview, downloadSkippedCsv } from './translate/corrections'; // Target schema validation -export { checkTargetTableSchema } from './translate/target-schema.js'; +export { checkTargetTableSchema } from './translate/target-schema'; // #endregion TranslateApi diff --git a/frontend/src/lib/api/translate/corrections.js b/frontend/src/lib/api/translate/corrections.js deleted file mode 100644 index c17c6131..00000000 --- a/frontend/src/lib/api/translate/corrections.js +++ /dev/null @@ -1,85 +0,0 @@ -// #region TranslateCorrectionsApi [C:2] [TYPE Module] [SEMANTICS translate, corrections, bulk-replace, api] -// @BRIEF API client for translation corrections — inline edits, dictionary submission, bulk find-and-replace, CSV download. -// @RELATION DEPENDS_ON -> [ApiModule] -import { api } from '$lib/api.js'; - -function normalizeTranslateError(error, defaultMessage = 'Translation API error') { - const message = - (error && typeof error.message === 'string' && error.message) || - (typeof error === 'string' && error) || - defaultMessage; - return { message, code: 'TRANSLATE_API_ERROR', retryable: true }; -} - -export async function submitCorrection(payload) { - try { - return await api.postApi('/translate/corrections', payload); - } catch (error) { - throw normalizeTranslateError(error, 'Failed to submit correction'); - } -} - -export async function submitBulkCorrections(payload) { - try { - return await api.postApi('/translate/corrections/bulk', payload); - } catch (error) { - throw normalizeTranslateError(error, 'Failed to submit bulk corrections'); - } -} - -export async function inlineEditCorrection(runId, recordId, languageCode, data) { - try { - return await api.requestApi( - `/translate/runs/${runId}/records/${recordId}/languages/${languageCode}`, - 'PUT', - data - ); - } catch (error) { - throw normalizeTranslateError(error, 'Failed to apply inline correction'); - } -} - -export async function submitCorrectionToDict(runId, recordId, languageCode, dictId) { - try { - return await api.requestApi( - `/translate/runs/${runId}/records/${recordId}/languages/${languageCode}/submit-to-dict`, - 'POST', - { dictionary_id: dictId } - ); - } catch (error) { - throw normalizeTranslateError(error, 'Failed to submit correction to dictionary'); - } -} - -export async function bulkFindReplace(runId, data) { - try { - return await api.postApi(`/translate/runs/${runId}/bulk-replace`, data); - } catch (error) { - throw normalizeTranslateError(error, 'Failed to perform bulk find-and-replace'); - } -} - -export async function bulkReplacePreview(runId, data) { - try { - return await api.postApi(`/translate/runs/${runId}/bulk-replace`, { ...data, preview: true }); - } catch (error) { - throw normalizeTranslateError(error, 'Failed to preview bulk find-and-replace'); - } -} - -export async function downloadSkippedCsv(runId) { - try { - const blob = await api.fetchApiBlob(`/translate/runs/${runId}/skipped.csv`); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `skipped-${runId.substring(0, 8)}.csv`; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - } catch (error) { - throw normalizeTranslateError(error, 'Failed to download skipped CSV'); - } -} -// #endregion TranslateCorrectionsApi diff --git a/frontend/src/lib/api/translate/corrections.ts b/frontend/src/lib/api/translate/corrections.ts new file mode 100644 index 00000000..8810f236 --- /dev/null +++ b/frontend/src/lib/api/translate/corrections.ts @@ -0,0 +1,137 @@ +// #region TranslateCorrectionsApi [C:2] [TYPE Module] [SEMANTICS translate, corrections, bulk-replace, api] +// @BRIEF API client for translation corrections — inline edits, dictionary submission, bulk find-and-replace, CSV download. +// @LAYER API +// @RELATION DEPENDS_ON -> [ApiModule] +// @POST All functions return typed responses or throw normalized TranslateApiError. + +import { api } from '$lib/api'; +import type { TranslateApiError } from './jobs'; + +function normalizeTranslateError(error: unknown, defaultMessage: string = 'Translation API error'): TranslateApiError { + const message = + (error && typeof (error as Error).message === 'string' && (error as Error).message) || + (typeof error === 'string' && error) || + defaultMessage; + return { message, code: 'TRANSLATE_API_ERROR' as const, retryable: true }; +} + +// #region submitCorrection [C:2] [TYPE Function] [SEMANTICS translate, corrections, submit] +// @BRIEF Submit a single correction for a translation record. +// @PRE payload contains record_id, language_code, and corrected_text. +// @POST Returns correction confirmation. +// @SIDE_EFFECT Updates the translation on the backend. +// @RELATION DEPENDS_ON -> [ApiModule.postApi] +export async function submitCorrection(payload: Record): Promise { + try { + return await api.postApi('/translate/corrections', payload); + } catch (error) { + throw normalizeTranslateError(error, 'Failed to submit correction'); + } +} +// #endregion submitCorrection + +// #region submitBulkCorrections [C:2] [TYPE Function] [SEMANTICS translate, corrections, bulk, submit] +// @BRIEF Submit multiple corrections in a single request. +// @PRE payload contains array of correction objects. +// @POST Returns bulk confirmation with per-item results. +// @SIDE_EFFECT Updates multiple translations on the backend. +// @RELATION DEPENDS_ON -> [ApiModule.postApi] +export async function submitBulkCorrections(payload: Record): Promise { + try { + return await api.postApi('/translate/corrections/bulk', payload); + } catch (error) { + throw normalizeTranslateError(error, 'Failed to submit bulk corrections'); + } +} +// #endregion submitBulkCorrections + +// #region inlineEditCorrection [C:2] [TYPE Function] [SEMANTICS translate, corrections, inline, edit] +// @BRIEF Apply an inline correction to a specific record+language combination in a run. +// @PRE runId, recordId, languageCode are non-empty strings. data contains corrected values. +// @POST Returns updated record data. +// @SIDE_EFFECT Modifies translation for the specified record+language. +// @RELATION DEPENDS_ON -> [ApiModule.requestApi] +export async function inlineEditCorrection(runId: string, recordId: string, languageCode: string, data: Record): Promise { + try { + return await api.requestApi( + `/translate/runs/${runId}/records/${recordId}/languages/${languageCode}`, + 'PUT', + data + ); + } catch (error) { + throw normalizeTranslateError(error, 'Failed to apply inline correction'); + } +} +// #endregion inlineEditCorrection + +// #region submitCorrectionToDict [C:2] [TYPE Function] [SEMANTICS translate, corrections, dictionary, submit] +// @BRIEF Submit a correction to be added as a dictionary entry. +// @PRE runId, recordId, languageCode, dictId are non-empty strings. +// @POST Creates a dictionary entry from the correction. +// @SIDE_EFFECT Adds entry to the specified dictionary on the backend. +// @RELATION DEPENDS_ON -> [ApiModule.requestApi] +export async function submitCorrectionToDict(runId: string, recordId: string, languageCode: string, dictId: string): Promise { + try { + return await api.requestApi( + `/translate/runs/${runId}/records/${recordId}/languages/${languageCode}/submit-to-dict`, + 'POST', + { dictionary_id: dictId } + ); + } catch (error) { + throw normalizeTranslateError(error, 'Failed to submit correction to dictionary'); + } +} +// #endregion submitCorrectionToDict + +// #region bulkFindReplace [C:2] [TYPE Function] [SEMANTICS translate, corrections, bulk, find-replace] +// @BRIEF Perform bulk find-and-replace across a translation run. +// @PRE runId is a non-empty string. data contains find/replace patterns. +// @POST Returns bulk operation results. +// @SIDE_EFFECT Applies find-and-replace to multiple records. +// @RELATION DEPENDS_ON -> [ApiModule.postApi] +export async function bulkFindReplace(runId: string, data: Record): Promise { + try { + return await api.postApi(`/translate/runs/${runId}/bulk-replace`, data); + } catch (error) { + throw normalizeTranslateError(error, 'Failed to perform bulk find-and-replace'); + } +} +// #endregion bulkFindReplace + +// #region bulkReplacePreview [C:2] [TYPE Function] [SEMANTICS translate, corrections, bulk, preview] +// @BRIEF Preview bulk find-and-replace operation (dry run, no changes applied). +// @PRE runId is a non-empty string. data contains find/replace patterns. +// @POST Returns preview with affected record count. No changes are persisted. +// @RELATION DEPENDS_ON -> [ApiModule.postApi] +export async function bulkReplacePreview(runId: string, data: Record): Promise { + try { + return await api.postApi(`/translate/runs/${runId}/bulk-replace`, { ...data, preview: true }); + } catch (error) { + throw normalizeTranslateError(error, 'Failed to preview bulk find-and-replace'); + } +} +// #endregion bulkReplacePreview + +// #region downloadSkippedCsv [C:2] [TYPE Function] [SEMANTICS translate, corrections, csv, download] +// @BRIEF Download a CSV of skipped records for a translation run. +// @PRE runId is a non-empty string. +// @POST Triggers browser download of a CSV file. Returns nothing on success. +// @SIDE_EFFECT Creates a Blob URL and programmatically clicks a download anchor. +// @RELATION DEPENDS_ON -> [ApiModule.fetchApiBlob] +export async function downloadSkippedCsv(runId: string): Promise { + try { + const blob = await api.fetchApiBlob(`/translate/runs/${runId}/skipped.csv`); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `skipped-${runId.substring(0, 8)}.csv`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } catch (error) { + throw normalizeTranslateError(error, 'Failed to download skipped CSV'); + } +} +// #endregion downloadSkippedCsv +// #endregion TranslateCorrectionsApi diff --git a/frontend/src/lib/api/translate/datasources.js b/frontend/src/lib/api/translate/datasources.js deleted file mode 100644 index ea1a6c2f..00000000 --- a/frontend/src/lib/api/translate/datasources.js +++ /dev/null @@ -1,89 +0,0 @@ -// #region TranslateDatasourcesApi [C:2] [TYPE Module] [SEMANTICS translate, datasources, columns, preview, api] -// @BRIEF API client for translation datasources — column fetching, preview, row-level review actions. -// @RELATION DEPENDS_ON -> [ApiModule] -import { api } from '$lib/api.js'; - -function normalizeTranslateError(error, defaultMessage = 'Translation API error') { - const message = - (error && typeof error.message === 'string' && error.message) || - (typeof error === 'string' && error) || - defaultMessage; - return { message, code: 'TRANSLATE_API_ERROR', retryable: true }; -} - -export async function fetchDatasources(envId, search = '') { - try { - return await api.fetchApi( - `/translate/datasources?env_id=${encodeURIComponent(envId)}&search=${encodeURIComponent(search)}` - ); - } catch (error) { - throw normalizeTranslateError(error, 'Failed to fetch datasources'); - } -} - -export async function fetchDatasourceColumns(datasourceId, envId) { - try { - return await api.fetchApi( - `/translate/datasources/${datasourceId}/columns?env_id=${encodeURIComponent(envId)}` - ); - } catch (error) { - throw normalizeTranslateError(error, 'Failed to fetch datasource columns'); - } -} - -export async function fetchPreview(jobId, sampleSize = 10, envId = '') { - try { - const body = { sample_size: sampleSize }; - if (envId) body.env_id = envId; - return await api.postApi(`/translate/jobs/${jobId}/preview`, body); - } catch (error) { - throw normalizeTranslateError(error, 'Failed to run preview'); - } -} - -export async function approveRow(jobId, rowKey, languageCode = null) { - try { - const body = { action: 'approve' }; - if (languageCode) body.language_code = languageCode; - return await api.requestApi(`/translate/jobs/${jobId}/preview/rows/${rowKey}`, 'PUT', body); - } catch (error) { - throw normalizeTranslateError(error, 'Failed to approve row'); - } -} - -export async function editRow(jobId, rowKey, translation, languageCode = null) { - try { - const body = { action: 'edit', translation }; - if (languageCode) body.language_code = languageCode; - return await api.requestApi(`/translate/jobs/${jobId}/preview/rows/${rowKey}`, 'PUT', body); - } catch (error) { - throw normalizeTranslateError(error, 'Failed to edit row'); - } -} - -export async function rejectRow(jobId, rowKey, languageCode = null) { - try { - const body = { action: 'reject' }; - if (languageCode) body.language_code = languageCode; - return await api.requestApi(`/translate/jobs/${jobId}/preview/rows/${rowKey}`, 'PUT', body); - } catch (error) { - throw normalizeTranslateError(error, 'Failed to reject row'); - } -} - -export async function acceptPreview(jobId) { - try { - return await api.postApi(`/translate/jobs/${jobId}/preview/accept`, {}); - } catch (error) { - throw normalizeTranslateError(error, 'Failed to accept preview'); - } -} - -export async function fetchPreviewRecords(sessionId) { - try { - return await api.fetchApi(`/translate/preview/${sessionId}/records`); - } catch (error) { - throw normalizeTranslateError(error, 'Failed to fetch preview records'); - } -} -// #endregion TranslateDatasourcesApi diff --git a/frontend/src/lib/api/translate/datasources.ts b/frontend/src/lib/api/translate/datasources.ts new file mode 100644 index 00000000..59594d7f --- /dev/null +++ b/frontend/src/lib/api/translate/datasources.ts @@ -0,0 +1,145 @@ +// #region TranslateDatasourcesApi [C:2] [TYPE Module] [SEMANTICS translate, datasources, columns, preview, api] +// @BRIEF API client for translation datasources — column fetching, preview, row-level review actions. +// @LAYER API +// @RELATION DEPENDS_ON -> [ApiModule] +// @POST All functions return typed responses or throw normalized TranslateApiError. + +import { api } from '$lib/api'; +import type { TranslateApiError } from './jobs'; + +function normalizeTranslateError(error: unknown, defaultMessage: string = 'Translation API error'): TranslateApiError { + const message = + (error && typeof (error as Error).message === 'string' && (error as Error).message) || + (typeof error === 'string' && error) || + defaultMessage; + return { message, code: 'TRANSLATE_API_ERROR' as const, retryable: true }; +} + +// #region fetchDatasources [C:2] [TYPE Function] [SEMANTICS translate, datasources, list] +// @BRIEF Fetch available datasources for a given environment. +// @PRE envId is a non-empty string. +// @POST Returns list of datasources matching the search. +// @RELATION DEPENDS_ON -> [ApiModule.fetchApi] +export async function fetchDatasources(envId: string, search: string = ''): Promise { + try { + return await api.fetchApi( + `/translate/datasources?env_id=${encodeURIComponent(envId)}&search=${encodeURIComponent(search)}` + ); + } catch (error) { + throw normalizeTranslateError(error, 'Failed to fetch datasources'); + } +} +// #endregion fetchDatasources + +// #region fetchDatasourceColumns [C:2] [TYPE Function] [SEMANTICS translate, datasources, columns] +// @BRIEF Fetch columns for a specific datasource. +// @PRE datasourceId and envId are non-empty strings. +// @POST Returns list of column definitions. +// @RELATION DEPENDS_ON -> [ApiModule.fetchApi] +export async function fetchDatasourceColumns(datasourceId: string, envId: string): Promise { + try { + return await api.fetchApi( + `/translate/datasources/${datasourceId}/columns?env_id=${encodeURIComponent(envId)}` + ); + } catch (error) { + throw normalizeTranslateError(error, 'Failed to fetch datasource columns'); + } +} +// #endregion fetchDatasourceColumns + +// #region fetchPreview [C:2] [TYPE Function] [SEMANTICS translate, preview, rows] +// @BRIEF Fetch a sample preview of rows for a translation job. +// @PRE jobId is a non-empty string. +// @POST Returns preview data with sample rows. +// @RELATION DEPENDS_ON -> [ApiModule.postApi] +export async function fetchPreview(jobId: string, sampleSize: number = 10, envId: string = ''): Promise { + try { + const body: Record = { sample_size: sampleSize }; + if (envId) body.env_id = envId; + return await api.postApi(`/translate/jobs/${jobId}/preview`, body); + } catch (error) { + throw normalizeTranslateError(error, 'Failed to run preview'); + } +} +// #endregion fetchPreview + +// #region approveRow [C:2] [TYPE Function] [SEMANTICS translate, preview, approve, row] +// @BRIEF Approve a specific preview row for translation. +// @PRE jobId and rowKey are non-empty strings. +// @POST Row is approved for translation. +// @SIDE_EFFECT Updates the row status on the backend. +// @RELATION DEPENDS_ON -> [ApiModule.requestApi] +export async function approveRow(jobId: string, rowKey: string, languageCode: string | null = null): Promise { + try { + const body: Record = { action: 'approve' }; + if (languageCode) body.language_code = languageCode; + return await api.requestApi(`/translate/jobs/${jobId}/preview/rows/${rowKey}`, 'PUT', body); + } catch (error) { + throw normalizeTranslateError(error, 'Failed to approve row'); + } +} +// #endregion approveRow + +// #region editRow [C:2] [TYPE Function] [SEMANTICS translate, preview, edit, row] +// @BRIEF Edit translation for a specific preview row. +// @PRE jobId and rowKey are non-empty strings. translation is a non-empty string. +// @POST Returns updated row data. +// @SIDE_EFFECT Modifies the translation on the backend. +// @RELATION DEPENDS_ON -> [ApiModule.requestApi] +export async function editRow(jobId: string, rowKey: string, translation: string, languageCode: string | null = null): Promise { + try { + const body: Record = { action: 'edit', translation }; + if (languageCode) body.language_code = languageCode; + return await api.requestApi(`/translate/jobs/${jobId}/preview/rows/${rowKey}`, 'PUT', body); + } catch (error) { + throw normalizeTranslateError(error, 'Failed to edit row'); + } +} +// #endregion editRow + +// #region rejectRow [C:2] [TYPE Function] [SEMANTICS translate, preview, reject, row] +// @BRIEF Reject a specific preview row (exclude from translation). +// @PRE jobId and rowKey are non-empty strings. +// @POST Row is marked as rejected and excluded from translation. +// @SIDE_EFFECT Updates the row status on the backend. +// @RELATION DEPENDS_ON -> [ApiModule.requestApi] +export async function rejectRow(jobId: string, rowKey: string, languageCode: string | null = null): Promise { + try { + const body: Record = { action: 'reject' }; + if (languageCode) body.language_code = languageCode; + return await api.requestApi(`/translate/jobs/${jobId}/preview/rows/${rowKey}`, 'PUT', body); + } catch (error) { + throw normalizeTranslateError(error, 'Failed to reject row'); + } +} +// #endregion rejectRow + +// #region acceptPreview [C:2] [TYPE Function] [SEMANTICS translate, preview, accept, finalize] +// @BRIEF Accept all approved rows in a preview session and start translation. +// @PRE jobId is a non-empty string with a finalized preview session. +// @POST Translation run is triggered for the accepted rows. +// @SIDE_EFFECT Creates translation run from accepted preview rows. +// @RELATION DEPENDS_ON -> [ApiModule.postApi] +export async function acceptPreview(jobId: string): Promise { + try { + return await api.postApi(`/translate/jobs/${jobId}/preview/accept`, {}); + } catch (error) { + throw normalizeTranslateError(error, 'Failed to accept preview'); + } +} +// #endregion acceptPreview + +// #region fetchPreviewRecords [C:2] [TYPE Function] [SEMANTICS translate, preview, records] +// @BRIEF Fetch records for a completed preview session. +// @PRE sessionId is a non-empty string. +// @POST Returns preview records with translated content. +// @RELATION DEPENDS_ON -> [ApiModule.fetchApi] +export async function fetchPreviewRecords(sessionId: string): Promise { + try { + return await api.fetchApi(`/translate/preview/${sessionId}/records`); + } catch (error) { + throw normalizeTranslateError(error, 'Failed to fetch preview records'); + } +} +// #endregion fetchPreviewRecords +// #endregion TranslateDatasourcesApi diff --git a/frontend/src/lib/api/translate/dictionaries.js b/frontend/src/lib/api/translate/dictionaries.js deleted file mode 100644 index 77ec6663..00000000 --- a/frontend/src/lib/api/translate/dictionaries.js +++ /dev/null @@ -1,109 +0,0 @@ -// #region TranslateDictionariesApi [C:2] [TYPE Module] [SEMANTICS translate, dictionaries, api, crud, entries] -// @BRIEF API client for translation dictionary CRUD — list, create, update, delete, entries, import. -// @RELATION DEPENDS_ON -> [ApiModule] -import { api } from '$lib/api.js'; - -function normalizeTranslateError(error, defaultMessage = 'Translation API error') { - const message = - (error && typeof error.message === 'string' && error.message) || - (typeof error === 'string' && error) || - defaultMessage; - return { message, code: 'TRANSLATE_API_ERROR', retryable: true }; -} - -export const dictionaryApi = { - async fetchDictionaries(options = {}) { - try { - const params = new URLSearchParams(); - if (options.page != null) params.append('page', String(options.page)); - if (options.page_size != null) params.append('page_size', String(options.page_size)); - const query = params.toString(); - return await api.fetchApi(`/translate/dictionaries${query ? `?${query}` : ''}`); - } catch (error) { - throw normalizeTranslateError(error, 'Failed to load dictionaries'); - } - }, - - async createDictionary(payload) { - try { - return await api.postApi('/translate/dictionaries', payload); - } catch (error) { - throw normalizeTranslateError(error, 'Failed to create dictionary'); - } - }, - - async getDictionary(dictionaryId) { - try { - return await api.fetchApi(`/translate/dictionaries/${dictionaryId}`); - } catch (error) { - throw normalizeTranslateError(error, 'Failed to load dictionary'); - } - }, - - async updateDictionary(dictionaryId, payload) { - try { - return await api.requestApi(`/translate/dictionaries/${dictionaryId}`, 'PUT', payload); - } catch (error) { - throw normalizeTranslateError(error, 'Failed to update dictionary'); - } - }, - - async deleteDictionary(dictionaryId) { - try { - return await api.deleteApi(`/translate/dictionaries/${dictionaryId}`); - } catch (error) { - throw normalizeTranslateError(error, 'Failed to delete dictionary'); - } - }, - - async fetchEntries(dictionaryId, options = {}) { - try { - const params = new URLSearchParams(); - if (options.page != null) params.append('page', String(options.page)); - if (options.page_size != null) params.append('page_size', String(options.page_size)); - const query = params.toString(); - return await api.fetchApi( - `/translate/dictionaries/${dictionaryId}/entries${query ? `?${query}` : ''}` - ); - } catch (error) { - throw normalizeTranslateError(error, 'Failed to load dictionary entries'); - } - }, - - async createEntry(dictionaryId, payload) { - try { - return await api.postApi(`/translate/dictionaries/${dictionaryId}/entries`, payload); - } catch (error) { - throw normalizeTranslateError(error, 'Failed to create dictionary entry'); - } - }, - - async updateEntry(dictionaryId, entryId, payload) { - try { - return await api.requestApi( - `/translate/dictionaries/${dictionaryId}/entries/${entryId}`, - 'PUT', - payload - ); - } catch (error) { - throw normalizeTranslateError(error, 'Failed to update dictionary entry'); - } - }, - - async deleteEntry(dictionaryId, entryId) { - try { - return await api.deleteApi(`/translate/dictionaries/${dictionaryId}/entries/${entryId}`); - } catch (error) { - throw normalizeTranslateError(error, 'Failed to delete dictionary entry'); - } - }, - - async importEntries(dictionaryId, payload) { - try { - return await api.postApi(`/translate/dictionaries/${dictionaryId}/import`, payload); - } catch (error) { - throw normalizeTranslateError(error, 'Failed to import dictionary entries'); - } - }, -}; -// #endregion TranslateDictionariesApi diff --git a/frontend/src/lib/api/translate/dictionaries.ts b/frontend/src/lib/api/translate/dictionaries.ts new file mode 100644 index 00000000..6a86c074 --- /dev/null +++ b/frontend/src/lib/api/translate/dictionaries.ts @@ -0,0 +1,145 @@ +// #region TranslateDictionariesApi [C:3] [TYPE Module] [SEMANTICS translate, dictionaries, api, crud, entries] +// @BRIEF API client for translation dictionary CRUD — list, create, update, delete, entries, import. +// @LAYER API +// @RELATION DEPENDS_ON -> [ApiModule] +// @POST All methods return typed responses or throw normalized TranslateApiError. + +import { api } from '$lib/api'; +import type { TranslateApiError } from './jobs'; + +function normalizeTranslateError(error: unknown, defaultMessage: string = 'Translation API error'): TranslateApiError { + const message = + (error && typeof (error as Error).message === 'string' && (error as Error).message) || + (typeof error === 'string' && error) || + defaultMessage; + return { message, code: 'TRANSLATE_API_ERROR' as const, retryable: true }; +} + +export interface PaginationOptions { + page?: number; + page_size?: number; +} + +// #region dictionaryApi [C:3] [TYPE Block] [SEMANTICS translate, dictionaries, api, crud] +// @BRIEF Dictionary CRUD operations — each method wraps api.* with error normalization. +// @RELATION DEPENDS_ON -> [ApiModule.fetchApi] +// @RELATION DEPENDS_ON -> [ApiModule.postApi] +// @RELATION DEPENDS_ON -> [ApiModule.requestApi] +// @RELATION DEPENDS_ON -> [ApiModule.deleteApi] +export const dictionaryApi = { + // #region fetchDictionaries [C:2] [TYPE Function] [SEMANTICS translate, dictionaries, list] + async fetchDictionaries(options: PaginationOptions = {}): Promise { + try { + const params = new URLSearchParams(); + if (options.page != null) params.append('page', String(options.page)); + if (options.page_size != null) params.append('page_size', String(options.page_size)); + const query = params.toString(); + return await api.fetchApi(`/translate/dictionaries${query ? `?${query}` : ''}`); + } catch (error) { + throw normalizeTranslateError(error, 'Failed to load dictionaries'); + } + }, + // #endregion fetchDictionaries + + // #region createDictionary [C:2] [TYPE Function] [SEMANTICS translate, dictionaries, create] + async createDictionary(payload: Record): Promise { + try { + return await api.postApi('/translate/dictionaries', payload); + } catch (error) { + throw normalizeTranslateError(error, 'Failed to create dictionary'); + } + }, + // #endregion createDictionary + + // #region getDictionary [C:2] [TYPE Function] [SEMANTICS translate, dictionaries, get] + async getDictionary(dictionaryId: string): Promise { + try { + return await api.fetchApi(`/translate/dictionaries/${dictionaryId}`); + } catch (error) { + throw normalizeTranslateError(error, 'Failed to load dictionary'); + } + }, + // #endregion getDictionary + + // #region updateDictionary [C:2] [TYPE Function] [SEMANTICS translate, dictionaries, update] + async updateDictionary(dictionaryId: string, payload: Record): Promise { + try { + return await api.requestApi(`/translate/dictionaries/${dictionaryId}`, 'PUT', payload); + } catch (error) { + throw normalizeTranslateError(error, 'Failed to update dictionary'); + } + }, + // #endregion updateDictionary + + // #region deleteDictionary [C:2] [TYPE Function] [SEMANTICS translate, dictionaries, delete] + async deleteDictionary(dictionaryId: string): Promise { + try { + return await api.deleteApi(`/translate/dictionaries/${dictionaryId}`); + } catch (error) { + throw normalizeTranslateError(error, 'Failed to delete dictionary'); + } + }, + // #endregion deleteDictionary + + // #region fetchEntries [C:2] [TYPE Function] [SEMANTICS translate, dictionaries, entries, list] + async fetchEntries(dictionaryId: string, options: PaginationOptions = {}): Promise { + try { + const params = new URLSearchParams(); + if (options.page != null) params.append('page', String(options.page)); + if (options.page_size != null) params.append('page_size', String(options.page_size)); + const query = params.toString(); + return await api.fetchApi( + `/translate/dictionaries/${dictionaryId}/entries${query ? `?${query}` : ''}` + ); + } catch (error) { + throw normalizeTranslateError(error, 'Failed to load dictionary entries'); + } + }, + // #endregion fetchEntries + + // #region createEntry [C:2] [TYPE Function] [SEMANTICS translate, dictionaries, entries, create] + async createEntry(dictionaryId: string, payload: Record): Promise { + try { + return await api.postApi(`/translate/dictionaries/${dictionaryId}/entries`, payload); + } catch (error) { + throw normalizeTranslateError(error, 'Failed to create dictionary entry'); + } + }, + // #endregion createEntry + + // #region updateEntry [C:2] [TYPE Function] [SEMANTICS translate, dictionaries, entries, update] + async updateEntry(dictionaryId: string, entryId: string, payload: Record): Promise { + try { + return await api.requestApi( + `/translate/dictionaries/${dictionaryId}/entries/${entryId}`, + 'PUT', + payload + ); + } catch (error) { + throw normalizeTranslateError(error, 'Failed to update dictionary entry'); + } + }, + // #endregion updateEntry + + // #region deleteEntry [C:2] [TYPE Function] [SEMANTICS translate, dictionaries, entries, delete] + async deleteEntry(dictionaryId: string, entryId: string): Promise { + try { + return await api.deleteApi(`/translate/dictionaries/${dictionaryId}/entries/${entryId}`); + } catch (error) { + throw normalizeTranslateError(error, 'Failed to delete dictionary entry'); + } + }, + // #endregion deleteEntry + + // #region importEntries [C:2] [TYPE Function] [SEMANTICS translate, dictionaries, entries, import] + async importEntries(dictionaryId: string, payload: Record): Promise { + try { + return await api.postApi(`/translate/dictionaries/${dictionaryId}/import`, payload); + } catch (error) { + throw normalizeTranslateError(error, 'Failed to import dictionary entries'); + } + }, + // #endregion importEntries +}; +// #endregion dictionaryApi +// #endregion TranslateDictionariesApi diff --git a/frontend/src/lib/api/translate/jobs.js b/frontend/src/lib/api/translate/jobs.js deleted file mode 100644 index 4c84aeca..00000000 --- a/frontend/src/lib/api/translate/jobs.js +++ /dev/null @@ -1,58 +0,0 @@ -// #region TranslateJobsApi [C:2] [TYPE Module] [SEMANTICS translate, jobs, api, crud] -// @BRIEF API client for translation job CRUD — list, create, update, delete, duplicate. -// @RELATION DEPENDS_ON -> [ApiModule] -import { api } from '$lib/api.js'; - -function normalizeTranslateError(error, defaultMessage = 'Translation API error') { - const message = - (error && typeof error.message === 'string' && error.message) || - (typeof error === 'string' && error) || - defaultMessage; - return { message, code: 'TRANSLATE_API_ERROR', retryable: true }; -} - -export async function fetchJobs(options = {}) { - try { - const params = new URLSearchParams(); - if (options.page != null) params.append('page', String(options.page)); - if (options.page_size != null) params.append('page_size', String(options.page_size)); - if (options.status) params.append('status', options.status); - const query = params.toString(); - return await api.fetchApi(`/translate/jobs${query ? `?${query}` : ''}`); - } catch (error) { - throw normalizeTranslateError(error, 'Failed to load translation jobs'); - } -} - -export async function createJob(payload) { - try { - return await api.postApi('/translate/jobs', payload); - } catch (error) { - throw normalizeTranslateError(error, 'Failed to create translation job'); - } -} - -export async function updateJob(jobId, payload) { - try { - return await api.requestApi(`/translate/jobs/${jobId}`, 'PUT', payload); - } catch (error) { - throw normalizeTranslateError(error, 'Failed to update translation job'); - } -} - -export async function deleteJob(jobId) { - try { - return await api.deleteApi(`/translate/jobs/${jobId}`); - } catch (error) { - throw normalizeTranslateError(error, 'Failed to delete translation job'); - } -} - -export async function duplicateJob(jobId) { - try { - return await api.postApi(`/translate/jobs/${jobId}/duplicate`, {}); - } catch (error) { - throw normalizeTranslateError(error, 'Failed to duplicate translation job'); - } -} -// #endregion TranslateJobsApi diff --git a/frontend/src/lib/api/translate/jobs.ts b/frontend/src/lib/api/translate/jobs.ts new file mode 100644 index 00000000..470e5718 --- /dev/null +++ b/frontend/src/lib/api/translate/jobs.ts @@ -0,0 +1,107 @@ +// #region TranslateJobsApi [C:2] [TYPE Module] [SEMANTICS translate, jobs, api, crud] +// @BRIEF API client for translation job CRUD — list, create, update, delete, duplicate. +// @LAYER API +// @RELATION DEPENDS_ON -> [ApiModule] +// @POST All functions return typed responses or throw normalized TranslateApiError. + +import { api } from '$lib/api'; + +export interface TranslateApiError { + message: string; + code: 'TRANSLATE_API_ERROR'; + retryable: boolean; +} + +function normalizeTranslateError(error: unknown, defaultMessage: string = 'Translation API error'): TranslateApiError { + const message = + (error && typeof (error as Error).message === 'string' && (error as Error).message) || + (typeof error === 'string' && error) || + defaultMessage; + return { message, code: 'TRANSLATE_API_ERROR' as const, retryable: true }; +} + +export interface JobQueryOptions { + page?: number; + page_size?: number; + status?: string; +} + +// #region fetchJobs [C:2] [TYPE Function] [SEMANTICS translate, jobs, list] +// @BRIEF Fetch paginated list of translation jobs. +// @PRE options may contain page/page_size/status filters. +// @POST Returns paginated job list response. +// @RELATION DEPENDS_ON -> [ApiModule.fetchApi] +export async function fetchJobs(options: JobQueryOptions = {}): Promise { + try { + const params = new URLSearchParams(); + if (options.page != null) params.append('page', String(options.page)); + if (options.page_size != null) params.append('page_size', String(options.page_size)); + if (options.status) params.append('status', options.status); + const query = params.toString(); + return await api.fetchApi(`/translate/jobs${query ? `?${query}` : ''}`); + } catch (error) { + throw normalizeTranslateError(error, 'Failed to load translation jobs'); + } +} +// #endregion fetchJobs + +// #region createJob [C:2] [TYPE Function] [SEMANTICS translate, jobs, create] +// @BRIEF Create a new translation job. +// @PRE payload contains valid job configuration. +// @POST Returns created job response. +// @SIDE_EFFECT Creates a new translation job on the backend. +// @RELATION DEPENDS_ON -> [ApiModule.postApi] +export async function createJob(payload: Record): Promise { + try { + return await api.postApi('/translate/jobs', payload); + } catch (error) { + throw normalizeTranslateError(error, 'Failed to create translation job'); + } +} +// #endregion createJob + +// #region updateJob [C:2] [TYPE Function] [SEMANTICS translate, jobs, update] +// @BRIEF Update an existing translation job. +// @PRE jobId is a non-empty string. payload contains fields to update. +// @POST Returns updated job response. +// @SIDE_EFFECT Modifies the job on the backend. +// @RELATION DEPENDS_ON -> [ApiModule.requestApi] +export async function updateJob(jobId: string, payload: Record): Promise { + try { + return await api.requestApi(`/translate/jobs/${jobId}`, 'PUT', payload); + } catch (error) { + throw normalizeTranslateError(error, 'Failed to update translation job'); + } +} +// #endregion updateJob + +// #region deleteJob [C:2] [TYPE Function] [SEMANTICS translate, jobs, delete] +// @BRIEF Delete a translation job. +// @PRE jobId is a non-empty string. +// @POST Returns success response. +// @SIDE_EFFECT Removes the job and associated runs from the backend. +// @RELATION DEPENDS_ON -> [ApiModule.deleteApi] +export async function deleteJob(jobId: string): Promise { + try { + return await api.deleteApi(`/translate/jobs/${jobId}`); + } catch (error) { + throw normalizeTranslateError(error, 'Failed to delete translation job'); + } +} +// #endregion deleteJob + +// #region duplicateJob [C:2] [TYPE Function] [SEMANTICS translate, jobs, duplicate] +// @BRIEF Duplicate a translation job (deep copy including configuration). +// @PRE jobId is a non-empty string. +// @POST Returns duplicated job response. +// @SIDE_EFFECT Creates a new job as a copy of the specified one. +// @RELATION DEPENDS_ON -> [ApiModule.postApi] +export async function duplicateJob(jobId: string): Promise { + try { + return await api.postApi(`/translate/jobs/${jobId}/duplicate`, {}); + } catch (error) { + throw normalizeTranslateError(error, 'Failed to duplicate translation job'); + } +} +// #endregion duplicateJob +// #endregion TranslateJobsApi diff --git a/frontend/src/lib/api/translate/runs.js b/frontend/src/lib/api/translate/runs.js deleted file mode 100644 index d1f7a271..00000000 --- a/frontend/src/lib/api/translate/runs.js +++ /dev/null @@ -1,127 +0,0 @@ -// #region TranslateRunsApi [C:2] [TYPE Module] [SEMANTICS translate, runs, api, control, status] -// @BRIEF API client for translation run control — trigger, status, history, records, retry, cancel, metrics. -// @RELATION DEPENDS_ON -> [ApiModule] -import { api } from '$lib/api.js'; - -function normalizeTranslateError(error, defaultMessage = 'Translation API error') { - const message = - (error && typeof error.message === 'string' && error.message) || - (typeof error === 'string' && error) || - defaultMessage; - return { message, code: 'TRANSLATE_API_ERROR', retryable: true }; -} - -export async function triggerRun(jobId, full = false) { - try { - const query = full ? '?full_translation=true' : ''; - return await api.postApi(`/translate/jobs/${jobId}/run${query}`, {}); - } catch (error) { - throw normalizeTranslateError(error, 'Failed to start translation run'); - } -} - -export async function fetchRunStatus(runId) { - try { - return await api.fetchApi(`/translate/runs/${runId}`); - } catch (error) { - throw normalizeTranslateError(error, 'Failed to fetch run status'); - } -} - -export async function fetchRunHistory(jobId, options = {}) { - try { - const params = new URLSearchParams(); - if (options.page != null) params.append('page', String(options.page)); - if (options.page_size != null) params.append('page_size', String(options.page_size)); - const query = params.toString(); - return await api.fetchApi(`/translate/jobs/${jobId}/runs${query ? `?${query}` : ''}`); - } catch (error) { - throw normalizeTranslateError(error, 'Failed to fetch run history'); - } -} - -export async function fetchRunRecords(runId, options = {}) { - try { - const params = new URLSearchParams(); - if (options.page != null) params.append('page', String(options.page)); - if (options.page_size != null) params.append('page_size', String(options.page_size)); - if (options.status) params.append('status', options.status); - const query = params.toString(); - return await api.fetchApi(`/translate/runs/${runId}/records${query ? `?${query}` : ''}`); - } catch (error) { - throw normalizeTranslateError(error, 'Failed to fetch run records'); - } -} - -export async function retryFailedBatches(runId) { - try { - return await api.postApi(`/translate/runs/${runId}/retry`, {}); - } catch (error) { - throw normalizeTranslateError(error, 'Failed to retry batches'); - } -} - -export async function retryInsert(runId) { - try { - return await api.postApi(`/translate/runs/${runId}/retry-insert`, {}); - } catch (error) { - throw normalizeTranslateError(error, 'Failed to retry insert'); - } -} - -export async function cancelRun(runId) { - try { - return await api.postApi(`/translate/runs/${runId}/cancel`, {}); - } catch (error) { - throw normalizeTranslateError(error, 'Failed to cancel run'); - } -} - -export async function fetchRunBatches(runId) { - try { - return await api.fetchApi(`/translate/runs/${runId}/batches`); - } catch (error) { - throw normalizeTranslateError(error, 'Failed to fetch run batches'); - } -} - -export async function fetchAllRuns(options = {}) { - try { - const params = new URLSearchParams(); - if (options.page != null) params.append('page', String(options.page)); - if (options.page_size != null) params.append('page_size', String(options.page_size)); - if (options.job_id) params.append('job_id', options.job_id); - if (options.status) params.append('status', options.status); - if (options.trigger_type) params.append('trigger_type', options.trigger_type); - if (options.created_by) params.append('created_by', options.created_by); - const query = params.toString(); - return await api.fetchApi(`/translate/runs${query ? `?${query}` : ''}`); - } catch (error) { - throw normalizeTranslateError(error, 'Failed to fetch runs'); - } -} - -export async function fetchRunDetail(runId) { - try { - return await api.fetchApi(`/translate/runs/${runId}/detail`); - } catch (error) { - throw normalizeTranslateError(error, 'Failed to fetch run detail'); - } -} - -export async function fetchJobMetrics(jobId) { - try { - return await api.fetchApi(`/translate/jobs/${jobId}/metrics`); - } catch (error) { - throw normalizeTranslateError(error, 'Failed to fetch job metrics'); - } -} - -export async function fetchAllMetrics() { - try { - return await api.fetchApi('/translate/metrics'); - } catch (error) { - throw normalizeTranslateError(error, 'Failed to fetch metrics'); - } -} -// #endregion TranslateRunsApi diff --git a/frontend/src/lib/api/translate/runs.ts b/frontend/src/lib/api/translate/runs.ts new file mode 100644 index 00000000..c1f267e1 --- /dev/null +++ b/frontend/src/lib/api/translate/runs.ts @@ -0,0 +1,221 @@ +// #region TranslateRunsApi [C:2] [TYPE Module] [SEMANTICS translate, runs, api, control, status] +// @BRIEF API client for translation run control — trigger, status, history, records, retry, cancel, metrics. +// @LAYER API +// @RELATION DEPENDS_ON -> [ApiModule] +// @POST All functions return typed responses or throw normalized TranslateApiError. + +import { api } from '$lib/api'; +import type { TranslateApiError } from './jobs'; + +function normalizeTranslateError(error: unknown, defaultMessage: string = 'Translation API error'): TranslateApiError { + const message = + (error && typeof (error as Error).message === 'string' && (error as Error).message) || + (typeof error === 'string' && error) || + defaultMessage; + return { message, code: 'TRANSLATE_API_ERROR' as const, retryable: true }; +} + +export interface RunQueryOptions { + page?: number; + page_size?: number; +} + +export interface RunHistoryQueryOptions extends RunQueryOptions { + status?: string; +} + +export interface AllRunsQueryOptions extends RunQueryOptions { + job_id?: string; + status?: string; + trigger_type?: string; + created_by?: string; +} + +// #region triggerRun [C:2] [TYPE Function] [SEMANTICS translate, runs, trigger] +// @BRIEF Trigger a new translation run for a job. +// @PRE jobId is a non-empty string. +// @POST Returns created run response with status. +// @SIDE_EFFECT Starts async translation processing on the backend. +// @RELATION DEPENDS_ON -> [ApiModule.postApi] +export async function triggerRun(jobId: string, full: boolean = false): Promise { + try { + const query = full ? '?full_translation=true' : ''; + return await api.postApi(`/translate/jobs/${jobId}/run${query}`, {}); + } catch (error) { + throw normalizeTranslateError(error, 'Failed to start translation run'); + } +} +// #endregion triggerRun + +// #region fetchRunStatus [C:2] [TYPE Function] [SEMANTICS translate, runs, status] +// @BRIEF Fetch the current status of a translation run. +// @PRE runId is a non-empty string. +// @POST Returns run status object with progress. +// @RELATION DEPENDS_ON -> [ApiModule.fetchApi] +export async function fetchRunStatus(runId: string): Promise { + try { + return await api.fetchApi(`/translate/runs/${runId}`); + } catch (error) { + throw normalizeTranslateError(error, 'Failed to fetch run status'); + } +} +// #endregion fetchRunStatus + +// #region fetchRunHistory [C:2] [TYPE Function] [SEMANTICS translate, runs, history] +// @BRIEF Fetch paginated run history for a job. +// @PRE jobId is a non-empty string. +// @POST Returns paginated run list. +// @RELATION DEPENDS_ON -> [ApiModule.fetchApi] +export async function fetchRunHistory(jobId: string, options: RunHistoryQueryOptions = {}): Promise { + try { + const params = new URLSearchParams(); + if (options.page != null) params.append('page', String(options.page)); + if (options.page_size != null) params.append('page_size', String(options.page_size)); + const query = params.toString(); + return await api.fetchApi(`/translate/jobs/${jobId}/runs${query ? `?${query}` : ''}`); + } catch (error) { + throw normalizeTranslateError(error, 'Failed to fetch run history'); + } +} +// #endregion fetchRunHistory + +// #region fetchRunRecords [C:2] [TYPE Function] [SEMANTICS translate, runs, records] +// @BRIEF Fetch paginated records for a translation run. +// @PRE runId is a non-empty string. +// @POST Returns paginated record list with translations. +// @RELATION DEPENDS_ON -> [ApiModule.fetchApi] +export async function fetchRunRecords(runId: string, options: RunQueryOptions & { status?: string } = {}): Promise { + try { + const params = new URLSearchParams(); + if (options.page != null) params.append('page', String(options.page)); + if (options.page_size != null) params.append('page_size', String(options.page_size)); + if (options.status) params.append('status', options.status); + const query = params.toString(); + return await api.fetchApi(`/translate/runs/${runId}/records${query ? `?${query}` : ''}`); + } catch (error) { + throw normalizeTranslateError(error, 'Failed to fetch run records'); + } +} +// #endregion fetchRunRecords + +// #region retryFailedBatches [C:2] [TYPE Function] [SEMANTICS translate, runs, retry, batches] +// @BRIEF Retry failed batches in a translation run. +// @PRE runId is a non-empty string. +// @POST Returns retry status. +// @SIDE_EFFECT Re-queues failed batches for processing. +// @RELATION DEPENDS_ON -> [ApiModule.postApi] +export async function retryFailedBatches(runId: string): Promise { + try { + return await api.postApi(`/translate/runs/${runId}/retry`, {}); + } catch (error) { + throw normalizeTranslateError(error, 'Failed to retry batches'); + } +} +// #endregion retryFailedBatches + +// #region retryInsert [C:2] [TYPE Function] [SEMANTICS translate, runs, retry, insert] +// @BRIEF Retry failed insert operations for a run. +// @PRE runId is a non-empty string. +// @POST Returns retry status. +// @SIDE_EFFECT Re-attempts failed DB insert operations. +// @RELATION DEPENDS_ON -> [ApiModule.postApi] +export async function retryInsert(runId: string): Promise { + try { + return await api.postApi(`/translate/runs/${runId}/retry-insert`, {}); + } catch (error) { + throw normalizeTranslateError(error, 'Failed to retry insert'); + } +} +// #endregion retryInsert + +// #region cancelRun [C:2] [TYPE Function] [SEMANTICS translate, runs, cancel] +// @BRIEF Cancel a running translation run. +// @PRE runId is a non-empty string referring to a RUNNING run. +// @POST Run is cancelled and no further batches will be processed. +// @SIDE_EFFECT Interrupts processing and marks run as CANCELLED. +// @RELATION DEPENDS_ON -> [ApiModule.postApi] +export async function cancelRun(runId: string): Promise { + try { + return await api.postApi(`/translate/runs/${runId}/cancel`, {}); + } catch (error) { + throw normalizeTranslateError(error, 'Failed to cancel run'); + } +} +// #endregion cancelRun + +// #region fetchRunBatches [C:2] [TYPE Function] [SEMANTICS translate, runs, batches] +// @BRIEF Fetch batch-level breakdown for a run. +// @PRE runId is a non-empty string. +// @POST Returns batch list with status and record counts. +// @RELATION DEPENDS_ON -> [ApiModule.fetchApi] +export async function fetchRunBatches(runId: string): Promise { + try { + return await api.fetchApi(`/translate/runs/${runId}/batches`); + } catch (error) { + throw normalizeTranslateError(error, 'Failed to fetch run batches'); + } +} +// #endregion fetchRunBatches + +// #region fetchAllRuns [C:2] [TYPE Function] [SEMANTICS translate, runs, all, list] +// @BRIEF Fetch all translation runs across jobs with optional filters. +// @POST Returns paginated cross-job run list. +// @RELATION DEPENDS_ON -> [ApiModule.fetchApi] +export async function fetchAllRuns(options: AllRunsQueryOptions = {}): Promise { + try { + const params = new URLSearchParams(); + if (options.page != null) params.append('page', String(options.page)); + if (options.page_size != null) params.append('page_size', String(options.page_size)); + if (options.job_id) params.append('job_id', options.job_id); + if (options.status) params.append('status', options.status); + if (options.trigger_type) params.append('trigger_type', options.trigger_type); + if (options.created_by) params.append('created_by', options.created_by); + const query = params.toString(); + return await api.fetchApi(`/translate/runs${query ? `?${query}` : ''}`); + } catch (error) { + throw normalizeTranslateError(error, 'Failed to fetch runs'); + } +} +// #endregion fetchAllRuns + +// #region fetchRunDetail [C:2] [TYPE Function] [SEMANTICS translate, runs, detail] +// @BRIEF Fetch detailed run information with aggregated stats. +// @PRE runId is a non-empty string. +// @POST Returns run detail with progress and record breakdown. +// @RELATION DEPENDS_ON -> [ApiModule.fetchApi] +export async function fetchRunDetail(runId: string): Promise { + try { + return await api.fetchApi(`/translate/runs/${runId}/detail`); + } catch (error) { + throw normalizeTranslateError(error, 'Failed to fetch run detail'); + } +} +// #endregion fetchRunDetail + +// #region fetchJobMetrics [C:2] [TYPE Function] [SEMANTICS translate, metrics, job] +// @BRIEF Fetch aggregate metrics for a translation job. +// @PRE jobId is a non-empty string. +// @POST Returns metrics object with counts and stats. +// @RELATION DEPENDS_ON -> [ApiModule.fetchApi] +export async function fetchJobMetrics(jobId: string): Promise { + try { + return await api.fetchApi(`/translate/jobs/${jobId}/metrics`); + } catch (error) { + throw normalizeTranslateError(error, 'Failed to fetch job metrics'); + } +} +// #endregion fetchJobMetrics + +// #region fetchAllMetrics [C:2] [TYPE Function] [SEMANTICS translate, metrics, all] +// @BRIEF Fetch aggregate metrics across all translation jobs. +// @POST Returns cross-job metrics. +// @RELATION DEPENDS_ON -> [ApiModule.fetchApi] +export async function fetchAllMetrics(): Promise { + try { + return await api.fetchApi('/translate/metrics'); + } catch (error) { + throw normalizeTranslateError(error, 'Failed to fetch metrics'); + } +} +// #endregion fetchAllMetrics +// #endregion TranslateRunsApi diff --git a/frontend/src/lib/api/translate/schedules.js b/frontend/src/lib/api/translate/schedules.js deleted file mode 100644 index 60d645b5..00000000 --- a/frontend/src/lib/api/translate/schedules.js +++ /dev/null @@ -1,61 +0,0 @@ -// #region TranslateSchedulesApi [C:2] [TYPE Module] [SEMANTICS translate, schedules, api, cron] -// @BRIEF API client for translation job scheduling — CRUD, enable/disable, next executions. -// @RELATION DEPENDS_ON -> [ApiModule] -import { api } from '$lib/api.js'; - -function normalizeTranslateError(error, defaultMessage = 'Translation API error') { - const message = - (error && typeof error.message === 'string' && error.message) || - (typeof error === 'string' && error) || - defaultMessage; - return { message, code: 'TRANSLATE_API_ERROR', retryable: true }; -} - -export async function fetchSchedule(jobId) { - try { - return await api.fetchApi(`/translate/jobs/${jobId}/schedule`, { suppressToast: true }); - } catch (error) { - throw normalizeTranslateError(error, 'Failed to fetch schedule'); - } -} - -export async function setSchedule(jobId, payload) { - try { - return await api.requestApi(`/translate/jobs/${jobId}/schedule`, 'PUT', payload); - } catch (error) { - throw normalizeTranslateError(error, 'Failed to set schedule'); - } -} - -export async function deleteSchedule(jobId) { - try { - return await api.deleteApi(`/translate/jobs/${jobId}/schedule`); - } catch (error) { - throw normalizeTranslateError(error, 'Failed to delete schedule'); - } -} - -export async function enableSchedule(jobId) { - try { - return await api.postApi(`/translate/jobs/${jobId}/schedule/enable`, {}); - } catch (error) { - throw normalizeTranslateError(error, 'Failed to enable schedule'); - } -} - -export async function disableSchedule(jobId) { - try { - return await api.postApi(`/translate/jobs/${jobId}/schedule/disable`, {}); - } catch (error) { - throw normalizeTranslateError(error, 'Failed to disable schedule'); - } -} - -export async function fetchNextExecutions(jobId, n = 3) { - try { - return await api.fetchApi(`/translate/jobs/${jobId}/schedule/next-executions?n=${n}`, { suppressToast: true }); - } catch (error) { - throw normalizeTranslateError(error, 'Failed to fetch next executions'); - } -} -// #endregion TranslateSchedulesApi diff --git a/frontend/src/lib/api/translate/schedules.ts b/frontend/src/lib/api/translate/schedules.ts new file mode 100644 index 00000000..2f2d0aae --- /dev/null +++ b/frontend/src/lib/api/translate/schedules.ts @@ -0,0 +1,105 @@ +// #region TranslateSchedulesApi [C:2] [TYPE Module] [SEMANTICS translate, schedules, api, cron] +// @BRIEF API client for translation job scheduling — CRUD, enable/disable, next executions. +// @LAYER API +// @RELATION DEPENDS_ON -> [ApiModule] +// @POST All functions return typed responses or throw normalized TranslateApiError. + +import { api } from '$lib/api'; +import type { TranslateApiError } from './jobs'; + +function normalizeTranslateError(error: unknown, defaultMessage: string = 'Translation API error'): TranslateApiError { + const message = + (error && typeof (error as Error).message === 'string' && (error as Error).message) || + (typeof error === 'string' && error) || + defaultMessage; + return { message, code: 'TRANSLATE_API_ERROR' as const, retryable: true }; +} + +// #region fetchSchedule [C:2] [TYPE Function] [SEMANTICS translate, schedules, fetch] +// @BRIEF Fetch the cron schedule for a translation job. +// @PRE jobId is a non-empty string. +// @POST Returns schedule configuration or null if no schedule exists. +// @RELATION DEPENDS_ON -> [ApiModule.fetchApi] +export async function fetchSchedule(jobId: string): Promise { + try { + return await api.fetchApi(`/translate/jobs/${jobId}/schedule`, { suppressToast: true }); + } catch (error) { + throw normalizeTranslateError(error, 'Failed to fetch schedule'); + } +} +// #endregion fetchSchedule + +// #region setSchedule [C:2] [TYPE Function] [SEMANTICS translate, schedules, set, update] +// @BRIEF Set or update the cron schedule for a translation job. +// @PRE jobId is a non-empty string. payload contains cron expression and enabled flag. +// @POST Returns updated schedule configuration. +// @SIDE_EFFECT Modifies the job's schedule on the backend. +// @RELATION DEPENDS_ON -> [ApiModule.requestApi] +export async function setSchedule(jobId: string, payload: Record): Promise { + try { + return await api.requestApi(`/translate/jobs/${jobId}/schedule`, 'PUT', payload); + } catch (error) { + throw normalizeTranslateError(error, 'Failed to set schedule'); + } +} +// #endregion setSchedule + +// #region deleteSchedule [C:2] [TYPE Function] [SEMANTICS translate, schedules, delete] +// @BRIEF Delete the cron schedule for a translation job. +// @PRE jobId is a non-empty string. +// @POST Schedule is removed. No more automated runs will be triggered. +// @SIDE_EFFECT Removes the schedule from the backend. +// @RELATION DEPENDS_ON -> [ApiModule.deleteApi] +export async function deleteSchedule(jobId: string): Promise { + try { + return await api.deleteApi(`/translate/jobs/${jobId}/schedule`); + } catch (error) { + throw normalizeTranslateError(error, 'Failed to delete schedule'); + } +} +// #endregion deleteSchedule + +// #region enableSchedule [C:2] [TYPE Function] [SEMANTICS translate, schedules, enable] +// @BRIEF Enable an existing schedule for a translation job. +// @PRE jobId is a non-empty string with an existing schedule. +// @POST Schedule is activated and will trigger runs per cron. +// @SIDE_EFFECT Enables the schedule on the backend. +// @RELATION DEPENDS_ON -> [ApiModule.postApi] +export async function enableSchedule(jobId: string): Promise { + try { + return await api.postApi(`/translate/jobs/${jobId}/schedule/enable`, {}); + } catch (error) { + throw normalizeTranslateError(error, 'Failed to enable schedule'); + } +} +// #endregion enableSchedule + +// #region disableSchedule [C:2] [TYPE Function] [SEMANTICS translate, schedules, disable] +// @BRIEF Disable an existing schedule without deleting it. +// @PRE jobId is a non-empty string with an existing schedule. +// @POST Schedule is deactivated. No runs will be triggered until re-enabled. +// @SIDE_EFFECT Disables the schedule on the backend. +// @RELATION DEPENDS_ON -> [ApiModule.postApi] +export async function disableSchedule(jobId: string): Promise { + try { + return await api.postApi(`/translate/jobs/${jobId}/schedule/disable`, {}); + } catch (error) { + throw normalizeTranslateError(error, 'Failed to disable schedule'); + } +} +// #endregion disableSchedule + +// #region fetchNextExecutions [C:2] [TYPE Function] [SEMANTICS translate, schedules, next-executions] +// @BRIEF Fetch upcoming execution times for a job's schedule. +// @PRE jobId is a non-empty string. +// @POST Returns array of upcoming datetime strings. +// @RELATION DEPENDS_ON -> [ApiModule.fetchApi] +export async function fetchNextExecutions(jobId: string, n: number = 3): Promise { + try { + return await api.fetchApi(`/translate/jobs/${jobId}/schedule/next-executions?n=${n}`, { suppressToast: true }); + } catch (error) { + throw normalizeTranslateError(error, 'Failed to fetch next executions'); + } +} +// #endregion fetchNextExecutions +// #endregion TranslateSchedulesApi diff --git a/frontend/src/lib/api/translate/target-schema.js b/frontend/src/lib/api/translate/target-schema.js deleted file mode 100644 index 2e3ff25e..00000000 --- a/frontend/src/lib/api/translate/target-schema.js +++ /dev/null @@ -1,49 +0,0 @@ -// #region TranslateTargetSchemaApi [C:1] [TYPE Module] [SEMANTICS translate, target-schema, api, validation] -// @BRIEF API client for target table schema validation. -// @RELATION DEPENDS_ON -> [ApiModule] -import { api } from '$lib/api.js'; - -/** - * Check target table schema — validate expected vs actual columns for translation inserts. - * @param {{ - * environment_id: string, - * target_database_id: string, - * target_schema: string, - * target_table: string, - * target_key_cols: string[], - * target_column: string|null, - * translation_column: string|null, - * target_language_column: string|null, - * target_source_column: string|null, - * target_source_language_column: string|null, - * }} payload - * @returns {Promise<{ - * table_exists: boolean, - * error: string|null, - * expected_columns: Array<{name: string, data_type: string|null, is_nullable: boolean}>, - * actual_columns: Array<{name: string, data_type: string|null, is_nullable: boolean}>, - * missing_columns: Array<{name: string, data_type: string|null, is_nullable: boolean}>, - * extra_columns: Array<{name: string, data_type: string|null, is_nullable: boolean}>, - * all_present: boolean, - * }>} - */ -export async function checkTargetTableSchema(payload) { - try { - return await api.postApi('/translate/check-target-schema', payload); - } catch (error) { - const message = - (error && typeof error.message === 'string' && error.message) || - (typeof error === 'string' && error) || - 'Failed to check target table schema'; - return { - table_exists: false, - error: message, - expected_columns: [], - actual_columns: [], - missing_columns: [], - extra_columns: [], - all_present: false, - }; - } -} -// #endregion TranslateTargetSchemaApi diff --git a/frontend/src/lib/api/translate/target-schema.ts b/frontend/src/lib/api/translate/target-schema.ts new file mode 100644 index 00000000..35a94a25 --- /dev/null +++ b/frontend/src/lib/api/translate/target-schema.ts @@ -0,0 +1,68 @@ +// #region TranslateTargetSchemaApi [C:2] [TYPE Module] [SEMANTICS translate, target-schema, api, validation] +// @BRIEF API client for target table schema validation — checks expected vs actual columns for translation inserts. +// @LAYER API +// @RELATION DEPENDS_ON -> [ApiModule] +// @POST Returns schema validation result with all_present flag, or a graceful error shape if the API call fails. + +import { api } from '$lib/api'; + +export interface TargetSchemaCheckPayload { + environment_id: string; + target_database_id: string; + target_schema: string; + target_table: string; + target_key_cols: string[]; + target_column: string | null; + translation_column: string | null; + target_language_column: string | null; + target_source_column: string | null; + target_source_language_column: string | null; +} + +export interface ColumnInfo { + name: string; + data_type: string | null; + is_nullable: boolean; +} + +export interface TargetSchemaCheckResponse { + table_exists: boolean; + error: string | null; + expected_columns: ColumnInfo[]; + actual_columns: ColumnInfo[]; + missing_columns: ColumnInfo[]; + extra_columns: ColumnInfo[]; + all_present: boolean; +} + +// #region checkTargetTableSchema [C:2] [TYPE Function] [SEMANTICS translate, target-schema, check, validate] +// @BRIEF Check target table schema — validate expected vs actual columns for translation inserts. +// @PRE payload contains all required target table configuration fields. +// @POST Returns schema validation result with all_present flag. +// On API failure, returns a graceful error shape (table_exists=false, error=message). +// @SIDE_EFFECT Sends POST request to validate target schema. +// @RELATION DEPENDS_ON -> [ApiModule.postApi] +// @RATIONALE On API failure, returns a graceful error object instead of throwing, +// because the schema check is advisory — the caller should still allow the +// user to proceed with a warning instead of blocking the flow entirely. +export async function checkTargetTableSchema(payload: TargetSchemaCheckPayload): Promise { + try { + return await api.postApi('/translate/check-target-schema', payload); + } catch (error) { + const message = + (error && typeof (error as Error).message === 'string' && (error as Error).message) || + (typeof error === 'string' && error) || + 'Failed to check target table schema'; + return { + table_exists: false, + error: message, + expected_columns: [], + actual_columns: [], + missing_columns: [], + extra_columns: [], + all_present: false, + }; + } +} +// #endregion checkTargetTableSchema +// #endregion TranslateTargetSchemaApi