// #region ApiModule [C:5] [TYPE Module] [SEMANTICS api, client, fetch, auth, error-handling] // @BRIEF Core API communication layer — typed fetch wrappers with auth injection, error normalization, toast feedback, and endpoint registry. // @LAYER Infrastructure // @RELATION DEPENDS_ON -> [ToastsModule] // @RELATION CALLED_BY -> [ReportsApi] // @RELATION CALLED_BY -> [AssistantApi] // @RELATION CALLED_BY -> [TranslateRunsApi] // @RELATION CALLED_BY -> [DatasetReviewApi] // @RELATION CALLED_BY -> [MaintenanceApi] // @RELATION CALLED_BY -> [ValidationRunDetailPageLoad] // @PRE Auth token is available in localStorage under 'auth_token' after login. // @POST Every API call returns typed JSON response or throws typed ApiError with status/detail/error_code. // @SIDE_EFFECT Reads localStorage for auth token on every request. Dispatches error toasts on non-suppressed failures. // @INVARIANT Every fetch MUST go through fetchApi/requestApi/postApi/deleteApi — never native fetch(). // @INVARIANT Every response.json() is wrapped in try/catch that converts HTTP errors to ApiError. // @INVARIANT FetchOptions.signal is passed to native fetch() for cancellation/timeout support. // @DATA_CONTRACT Input: (endpoint, body?, options?) → Output: Promise | Promise | Error(ApiError) // @RATIONALE FetchOptions.signal added per semantics-core anti-loop protocol — prevents infinite loading // state when backend is unreachable. Components can now pass AbortSignal.timeout() or // onDestroy-aborted signals to cancel in-flight requests. // @RATIONALE fetch wrappers exist to enforce auth token injection, centralized error handling, // toast feedback, and trace_id propagation — without wrapping every component in try/catch boilerplate. // @REJECTED Native fetch() rejected — would bypass auth header injection, error normalization, // and toast feedback. Every component would need its own error handling. // @REJECTED Axios rejected — unnecessary dependency for a single-domain API client; native fetch + wrappers // is simpler, tree-shakeable, and has zero bundle cost. import { log, setTraceId, getTraceId } from '$lib/cot-logger'; import { addToast } from './toasts.svelte.js'; import type { FetchOptions, DashboardListParams } from '../types/api'; const API_BASE_URL = '/api'; /** Default timeout for API requests in milliseconds (30s). Consumer may override via AbortSignal.timeout(). */ export const API_REQUEST_TIMEOUT = 30_000; // #region ApiTypes [C:1] [TYPE Block] [SEMANTICS api, types, interfaces] // @BRIEF Internal type definitions for error handling and API configuration. // @DATA_CONTRACT ApiError: { message, status, detail, error_code } interface ApiError extends Error { status?: number; detail?: unknown; error_code?: string; } interface BuildApiErrorResponse { detail?: string | { message?: string; error_code?: string; [k: string]: unknown }; error_code?: string; [k: string]: unknown; } // #endregion ApiTypes // #region buildApiError [C:2] [TYPE Function] [SEMANTICS api, error, parsing] // @BRIEF Parse an HTTP Response into a structured ApiError with status, detail, and error_code. // @PRE response is a failed HTTP Response object (ok === false). // @POST Returns ApiError with message, status, detail, and optional error_code extracted from response body. // @SIDE_EFFECT Reads response body via .json() (consumes the response stream). // @RELATION CALLED_BY -> [fetchApi] // @RELATION CALLED_BY -> [postApi] // @RELATION CALLED_BY -> [deleteApi] // @RELATION CALLED_BY -> [requestApi] // @RATIONALE JSON body parsing is wrapped in .catch() because some error responses (e.g. 502 proxy errors) // return non-JSON bodies. Without this, the error itself would throw and mask the original status code. async function buildApiError(response: Response): Promise { const errorData: BuildApiErrorResponse = await response.json().catch(() => ({})); const detail = errorData?.detail; const message = detail ? (typeof detail === 'string' ? detail : (typeof detail?.message === 'string' ? detail.message : JSON.stringify(detail))) : `API request failed with status ${response.status}`; const error: ApiError = new Error(message) as ApiError; error.status = response.status; error.detail = detail; if (detail && typeof detail === 'object' && (detail as Record).error_code) { error.error_code = String((detail as Record).error_code); } return error; } // #endregion buildApiError // #region notifyApiError [C:2] [TYPE Function] [SEMANTICS api, error, toast, feedback] // @BRIEF Dispatch an error toast with severity-based messaging. // @PRE error is a structured ApiError (may have status). // @POST Toast is dispatched with appropriate message and error severity. // @SIDE_EFFECT Calls addToast() which mutates the global toast store. // @RELATION DEPENDS_ON -> [addToast:Function] // @RELATION CALLED_BY -> [fetchApi] // @RELATION CALLED_BY -> [postApi] // @RELATION CALLED_BY -> [deleteApi] // @RELATION CALLED_BY -> [requestApi] // @UX_FEEDBACK 401 → "401 Unauthorized" toast. 500+ → "Server error (N)" toast. Others → error.message toast. function notifyApiError(error: ApiError): void { if (error?.status === 401) { addToast(`401 Unauthorized: ${error.message}`, 'error'); return; } if (error?.status >= 500) { addToast(`Server error (${error.status}): ${error.message}`, 'error'); return; } addToast(error.message, 'error'); } // #endregion notifyApiError // #region shouldSuppressApiErrorToast [C:2] [TYPE Function] [SEMANTICS api, error, suppression, heuristics] // @BRIEF Determine whether an API error should be silently suppressed (no toast) based on endpoint + error heuristics. // @PRE endpoint is a string path. error is a structured ApiError. // @POST Returns boolean — true if the error is an expected "non-error" for the given endpoint. // @RATIONALE Several endpoints legitimately return 4xx for "empty" states (no git repo, no clarification session). // Suppressing these toasts prevents noise pollution while still throwing for programmatic handling. // @REJECTED A global "ignore 4xx" flag rejected — too broad. Each heuristic is endpoint-specific. // @RELATION CALLED_BY -> [requestApi] function shouldSuppressApiErrorToast(endpoint: string, error: ApiError): boolean { const isGitStatusEndpoint = typeof endpoint === 'string' && endpoint.startsWith('/git/repositories/') && endpoint.endsWith('/status'); const isNoRepoError = (error?.status === 400 || error?.status === 404) && /Repository for dashboard .* not found/i.test(String(error?.message || '')); const isGitPullEndpoint = typeof endpoint === 'string' && endpoint.startsWith('/git/repositories/') && endpoint.endsWith('/pull'); const isUnfinishedMergeError = error?.status === 409 && (String(error?.error_code || '') === 'GIT_UNFINISHED_MERGE' || String((error?.detail as Record)?.error_code || '') === 'GIT_UNFINISHED_MERGE'); const isDatasetClarificationEndpoint = typeof endpoint === 'string' && /\/dataset-orchestration\/sessions\/[^/]+\/clarification$/.test(endpoint); const isMissingClarificationSession = error?.status === 404 && /Clarification session not found/i.test(String(error?.message || '')); const isGitRepoEndpoint = typeof endpoint === 'string' && endpoint.startsWith('/git/repositories/'); const isMissingEnvIdError = error?.status === 400 && /env_id is required/i.test(String(error?.message || '')); const isGitConfigReposEndpoint = typeof endpoint === 'string' && /^\/git\/config\/[^/]+\/repositories$/.test(endpoint); const isRepoAlreadyExistsError = error?.status === 409 && /already exists/i.test(String(error?.message || '')); return (isGitStatusEndpoint && isNoRepoError) || (isGitPullEndpoint && isUnfinishedMergeError) || (isDatasetClarificationEndpoint && isMissingClarificationSession) || (isGitRepoEndpoint && isMissingEnvIdError) || (isGitConfigReposEndpoint && isRepoAlreadyExistsError); } // #endregion shouldSuppressApiErrorToast // #region wsUrlHelpers [C:2] [TYPE Block] [SEMANTICS websocket, url, task-logs, maintenance, translate] // @BRIEF WebSocket URL builders — each constructs an authenticated WS endpoint for a specific channel. // @PRE taskId / runId are non-empty strings where applicable. // @POST Returns fully-qualified ws:// or wss:// URL with auth token as query parameter. // @SIDE_EFFECT Reads localStorage for auth_token on each call. // @RATIONALE WebSocket API does not support custom headers in browser, so auth token is appended as query param. /** * Build a WebSocket URL for a task's log stream, including auth token. */ export const getWsUrl = (taskId: string): string => { const protocol = typeof window !== 'undefined' && window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const host = typeof window !== 'undefined' ? window.location.host : 'localhost:8000'; let url = `${protocol}//${host}/ws/logs/${taskId}`; if (typeof window !== 'undefined') { const token = localStorage.getItem('auth_token'); if (token) { url += `?token=${encodeURIComponent(token)}`; } } return url; }; /** * Build a WebSocket URL for global task events, including auth token. */ export const getTaskEventsWsUrl = (): string => { const protocol = typeof window !== 'undefined' && window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const host = typeof window !== 'undefined' ? window.location.host : 'localhost:8000'; let url = `${protocol}//${host}/ws/task-events`; if (typeof window !== 'undefined') { const token = localStorage.getItem('auth_token'); if (token) { url += `?token=${encodeURIComponent(token)}`; } } return url; }; /** * Build a WebSocket URL for maintenance events, including auth token. */ // #region getMaintenanceEventsWsUrl [C:1] [TYPE Function] [SEMANTICS api, ws, maintenance, url] export const getMaintenanceEventsWsUrl = (): string => { const protocol = typeof window !== 'undefined' && window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const host = typeof window !== 'undefined' ? window.location.host : 'localhost:8000'; let url = `${protocol}//${host}/ws/maintenance/events`; if (typeof window !== 'undefined') { const token = localStorage.getItem('auth_token'); if (token) { url += `?token=${encodeURIComponent(token)}`; } } return url; }; // #endregion getMaintenanceEventsWsUrl /** * Build a WebSocket URL for translation run progress streaming. */ // #region getTranslateRunWsUrl [C:1] [TYPE Function] [SEMANTICS api, ws, translate, run, url] export const getTranslateRunWsUrl = (runId: string): string => { const protocol = typeof window !== 'undefined' && window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const host = typeof window !== 'undefined' ? window.location.host : 'localhost:8000'; let url = `${protocol}//${host}/ws/translate/run/${runId}`; if (typeof window !== 'undefined') { const token = localStorage.getItem('auth_token'); if (token) { url += `?token=${encodeURIComponent(token)}`; } } return url; }; // #endregion getTranslateRunWsUrl // #endregion wsUrlHelpers // #region getAuthHeaders [C:2] [TYPE Function] [SEMANTICS auth, headers, token, localStorage] // @BRIEF Build request headers with Content-Type and optional Bearer token from localStorage. // @PRE extraHeaders is an optional map of header key → value. // @POST Returns headers object with Content-Type: application/json and Authorization: Bearer if logged in. // @SIDE_EFFECT Reads localStorage for auth_token on every call. // @RELATION CALLED_BY -> [fetchApi] // @RELATION CALLED_BY -> [fetchApiBlob] // @RELATION CALLED_BY -> [postApi] // @RELATION CALLED_BY -> [deleteApi] // @RELATION CALLED_BY -> [requestApi] function getAuthHeaders(extraHeaders: Record = {}): Record { const headers: Record = { 'Content-Type': 'application/json', ...extraHeaders }; if (typeof window !== 'undefined') { const token = localStorage.getItem('auth_token'); if (token) headers['Authorization'] = `Bearer ${token}`; // Propagate trace_id to backend for cross-stack correlation const tid = getTraceId(); if (tid && tid !== 'no-trace') { headers['X-Trace-ID'] = tid; } } return headers; } // #endregion getAuthHeaders // #region fetchApi [C:4] [TYPE Function] [SEMANTICS api, fetch, get, json] // @BRIEF Perform an authenticated GET request and return typed JSON response. // @PRE endpoint is a non-empty string path (without /api prefix). // @PRE options.headers may override default Content-Type. // @POST Returns Promise with parsed JSON. Returns null for 204 No Content. // Throws ApiError on non-2xx response. // @SIDE_EFFECT Sends HTTP GET request. On failure, dispatches error toast (unless options.suppressToast). // Writes CoT log line via log() on entry and failure. // @RELATION DEPENDS_ON -> [buildApiError] // @RELATION DEPENDS_ON -> [notifyApiError] // @RELATION DEPENDS_ON -> [getAuthHeaders] /** Endpoints that are polled frequently — suppress CoT REASON/REFLECT to reduce noise. * Errors are still logged as EXPLORE. Matches backend's log_requests polling suppression. */ const _SILENT_POLLING_ENDPOINTS = [ '/health/summary', '/api/tasks', ]; /** Check if an endpoint is a silent polling endpoint (suppress CoT REASON/REFLECT). */ function _isSilentPolling(endpoint: string): boolean { return _SILENT_POLLING_ENDPOINTS.some(e => endpoint.startsWith(e) || endpoint.endsWith(e)); } /** Extract X-Trace-Id from response headers and seed the CoT logger's trace_id. */ function _captureTraceId(response: Response): void { try { const traceId = response.headers?.get?.('x-trace-id'); if (traceId) setTraceId(traceId); } catch { // headers unavailable (e.g. test mock without full Headers API) } } async function fetchApi(endpoint: string, options: FetchOptions = {}): Promise { const _start = performance.now(); const _silent = _isSilentPolling(endpoint); try { if (!_silent) log('ApiClient', 'REASON', 'GET data', { endpoint }); const fetchInit: RequestInit = { headers: getAuthHeaders(options.headers || {}) }; if (options.signal) fetchInit.signal = options.signal; const response = await fetch(`${API_BASE_URL}${endpoint}`, fetchInit); if (!response.ok) throw await buildApiError(response); if (response.status === 204) return null as T; _captureTraceId(response); const data = await response.json() as T; if (!_silent) log('ApiClient', 'REFLECT', 'GET completed', { endpoint, status: response.status, elapsed_ms: Math.round(performance.now() - _start), }); return data; } catch (error) { const apiError = error as ApiError; log('ApiClient', 'EXPLORE', 'GET failed', { endpoint }, apiError?.message || 'unknown'); if (!options.suppressToast) notifyApiError(apiError); throw error; } } // #endregion fetchApi // #region fetchApiBlob [C:4] [TYPE Function] [SEMANTICS api, fetch, blob, thumbnail, file] // @BRIEF Perform an authenticated GET request and return a Blob (for thumbnails, file downloads). // @PRE endpoint is a non-empty string path. // @POST Returns Promise with binary data. Throws ApiError on failure or 202 "in progress". // @SIDE_EFFECT Sends HTTP GET request. On failure (unless notifyError=false), dispatches error toast. // @RELATION DEPENDS_ON -> [buildApiError] // @RELATION DEPENDS_ON -> [notifyApiError] // @RELATION DEPENDS_ON -> [getAuthHeaders] // @RATIONALE 202 status is handled as a special case — the thumbnail generation may still be in progress. // The caller can retry after a delay. This is NOT treated as a server error. async function fetchApiBlob(endpoint: string, options: FetchOptions = {}): Promise { const notifyError = options.notifyError !== false; try { const fetchInit: RequestInit = { headers: getAuthHeaders(options.headers || {}) }; if (options.signal) fetchInit.signal = options.signal; const response = await fetch(`${API_BASE_URL}${endpoint}`, fetchInit); if (response.status === 202) { const payload: Record = await response.json().catch(() => ({ message: "Resource is being prepared" })); const error: ApiError = new Error((payload?.message as string) || "Resource is being prepared") as ApiError; error.status = 202; throw error; } if (!response.ok) throw await buildApiError(response); return await response.blob(); } catch (error) { const apiError = error as ApiError; if (notifyError) notifyApiError(apiError); throw error; } } // #endregion fetchApiBlob // #region postApi [C:4] [TYPE Function] [SEMANTICS api, post, create, json] // @BRIEF Perform an authenticated POST request with JSON body and return typed response. // @PRE endpoint is a non-empty string path. // @PRE body is JSON-serializable (will be passed through JSON.stringify). // @POST Returns Promise with parsed JSON. Returns null for 204 No Content. // Throws ApiError on non-2xx response. // @SIDE_EFFECT Sends HTTP POST request. On failure, dispatches error toast (unless options.suppressToast). // Writes CoT log line on entry and failure. // @RELATION DEPENDS_ON -> [buildApiError] // @RELATION DEPENDS_ON -> [notifyApiError] // @RELATION DEPENDS_ON -> [getAuthHeaders] async function postApi(endpoint: string, body: unknown, options: FetchOptions = {}): Promise { const _start = performance.now(); const _silent = _isSilentPolling(endpoint); try { if (!_silent) log('ApiClient', 'REASON', 'POST data', { endpoint }); const fetchInit: RequestInit = { method: 'POST', headers: getAuthHeaders(options.headers || {}), body: JSON.stringify(body), }; if (options.signal) fetchInit.signal = options.signal; const response = await fetch(`${API_BASE_URL}${endpoint}`, fetchInit); if (!response.ok) throw await buildApiError(response); if (response.status === 204) return null as T; _captureTraceId(response); const data = await response.json() as T; if (!_silent) log('ApiClient', 'REFLECT', 'POST completed', { endpoint, status: response.status, elapsed_ms: Math.round(performance.now() - _start), }); return data; } catch (error) { const apiError = error as ApiError; log('ApiClient', 'EXPLORE', 'POST failed', { endpoint }, apiError?.message || 'unknown'); if (!options.suppressToast) notifyApiError(apiError); throw error; } } // #endregion postApi // #region deleteApi [C:4] [TYPE Function] [SEMANTICS api, delete, remove] // @BRIEF Perform an authenticated DELETE request and return typed response. // @PRE endpoint is a non-empty string path. // @POST Returns Promise with parsed JSON. Returns null for 204 No Content. // Throws ApiError on non-2xx response. Always dispatches error toast on failure. // @SIDE_EFFECT Sends HTTP DELETE request. On failure, dispatches error toast. // Writes CoT log line on entry and failure. // @RELATION DEPENDS_ON -> [buildApiError] // @RELATION DEPENDS_ON -> [notifyApiError] // @RELATION DEPENDS_ON -> [getAuthHeaders] // @RATIONALE deleteApi always notifies on error (no suppressToast option) because deletions // are destructive operations — the user must know if one failed. async function deleteApi(endpoint: string, options: FetchOptions = {}): Promise { const _start = performance.now(); const _silent = _isSilentPolling(endpoint); try { if (!_silent) log('ApiClient', 'REASON', 'DELETE data', { endpoint }); const fetchInit: RequestInit = { method: 'DELETE', headers: getAuthHeaders(options.headers || {}) }; if (options.signal) fetchInit.signal = options.signal; const response = await fetch(`${API_BASE_URL}${endpoint}`, fetchInit); if (!response.ok) throw await buildApiError(response); if (response.status === 204) return null as T; _captureTraceId(response); const data = await response.json() as T; if (!_silent) log('ApiClient', 'REFLECT', 'DELETE completed', { endpoint, status: response.status, elapsed_ms: Math.round(performance.now() - _start), }); return data; } catch (error) { const apiError = error as ApiError; log('ApiClient', 'EXPLORE', 'DELETE failed', { endpoint }, apiError?.message || 'unknown'); notifyApiError(apiError); throw error; } } // #endregion deleteApi // #region requestApi [C:4] [TYPE Function] [SEMANTICS api, request, generic, patch, put] // @BRIEF Generic authenticated HTTP request — supports any method, optional JSON body, and contextual toast suppression. // @PRE endpoint is a non-empty string path. // @PRE body is JSON-serializable if provided (null = no body). // @POST Returns Promise with parsed JSON. Returns null for 204 No Content. // Throws ApiError on non-2xx response. // Error toast suppressed if shouldSuppressApiErrorToast returns true for the endpoint+error combo. // @SIDE_EFFECT Sends HTTP request. On failure, conditionally dispatches error toast (subject to suppression heuristics). // Writes CoT log line on entry and failure. // @RELATION DEPENDS_ON -> [buildApiError] // @RELATION DEPENDS_ON -> [notifyApiError] // @RELATION DEPENDS_ON -> [shouldSuppressApiErrorToast] // @RELATION DEPENDS_ON -> [getAuthHeaders] async function requestApi(endpoint: string, method: string = 'GET', body: unknown = null, requestOptions: FetchOptions = {}): Promise { const _start = performance.now(); const _silent = _isSilentPolling(endpoint); try { if (!_silent) log('ApiClient', 'REASON', `${method} data`, { endpoint, method }); const fetchInit: RequestInit = { method, headers: getAuthHeaders(requestOptions.headers || {}) }; if (body) fetchInit.body = JSON.stringify(body); if (requestOptions.signal) fetchInit.signal = requestOptions.signal; const response = await fetch(`${API_BASE_URL}${endpoint}`, fetchInit); if (!response.ok) throw await buildApiError(response); if (response.status === 204) return null as T; _captureTraceId(response); const data = await response.json() as T; if (!_silent) log('ApiClient', 'REFLECT', `${method} completed`, { endpoint, method, status: response.status, elapsed_ms: Math.round(performance.now() - _start), }); return data; } catch (error) { const apiError = error as ApiError; log('ApiClient', 'EXPLORE', `${method} failed`, { method, endpoint }, apiError?.message || 'unknown'); if (!requestOptions.suppressToast && !shouldSuppressApiErrorToast(endpoint, apiError)) { notifyApiError(apiError); } throw error; } } // #endregion requestApi // ── Named type helpers for API methods ─────────────────────── // These are internal interfaces used by the api registry below. interface TaskListQueryOptions { limit?: number; offset?: number; status?: string; task_type?: string; completed_only?: boolean; plugin_id?: string[]; search?: string; } interface TaskLogQueryOptions { level?: string; source?: string; search?: string; offset?: number; limit?: number; } interface DashboardThumbnailOptions { force?: boolean; } interface DatasetQueryOptions { search?: string; filter?: string; page?: string; page_size?: string; } interface SupersetAccountOptions { search?: string; page_index?: number; page_size?: number; sort_column?: string; sort_order?: string; } interface ValidationTaskQueryParams { page?: number; page_size?: number; is_active?: boolean; environment_id?: string; search?: string; } interface ValidationRunQueryParams { page?: number; page_size?: number; } // #region ApiRegistry [C:3] [TYPE Block] [SEMANTICS api, endpoints, registry] // @BRIEF Named endpoint registry — maps backend API paths to typed frontend methods. // @LAYER API // @RELATION DEPENDS_ON -> [fetchApi] // @RELATION DEPENDS_ON -> [postApi] // @RELATION DEPENDS_ON -> [deleteApi] // @RELATION DEPENDS_ON -> [requestApi] // @RELATION DEPENDS_ON -> [fetchApiBlob] // @INVARIANT Every method delegates to fetchApi/postApi/deleteApi/requestApi — never native fetch. // @RATIONALE The registry pattern keeps endpoint paths in one place and eliminates path-string duplication across components. export const api = { fetchApi: fetchApi as (endpoint: string, options?: FetchOptions) => Promise, postApi: postApi as (endpoint: string, body: unknown, options?: FetchOptions) => Promise, deleteApi: deleteApi as (endpoint: string, options?: FetchOptions) => Promise, requestApi: requestApi as (endpoint: string, method?: string, body?: unknown, requestOptions?: FetchOptions) => Promise, fetchApiBlob: fetchApiBlob as (endpoint: string, options?: FetchOptions) => Promise, // ═══ Tasks ════════════════════════════════════════════════════ // #region getPlugins [C:2] [TYPE Function] [SEMANTICS plugins,api,list] // @BRIEF Fetch all registered plugins. // @LAYER API // @RELATION DEPENDS_ON -> [fetchApi] // @DATA_CONTRACT response -> { plugins: { id, name, description, version, category }[] } getPlugins: () => fetchApi('/plugins'), // #endregion getPlugins // #region getTasks [C:2] [TYPE Function] [SEMANTICS tasks,api,list,pagination] // @BRIEF Fetch paginated task list with optional status/type filters. // @LAYER API // @RELATION DEPENDS_ON -> [fetchApi] // @DATA_CONTRACT params -> { limit?, offset?, status?, task_type?, completed_only?, plugin_id?[] } // @DATA_CONTRACT response -> { tasks: { id, plugin_id, status, started_at, completed_at, progress?, error?, retry_count?, params? }[], total: number } getTasks: (options: TaskListQueryOptions = {}) => { const params = new URLSearchParams(); if (options.limit != null) params.append('limit', String(options.limit)); if (options.offset != null) params.append('offset', String(options.offset)); if (options.status) params.append('status', options.status); if (options.task_type) params.append('task_type', options.task_type); if (options.completed_only != null) params.append('completed_only', String(Boolean(options.completed_only))); if (Array.isArray(options.plugin_id)) options.plugin_id.forEach((pid: string) => params.append('plugin_id', pid)); if (options.search) params.append('search', options.search); const query = params.toString(); return fetchApi(`/tasks${query ? `?${query}` : ''}`); }, // #endregion getTasks // #region getTask [C:2] [TYPE Function] [SEMANTICS tasks,api,detail] // @BRIEF Fetch a single task by ID with full status and result. // @LAYER API // @RELATION DEPENDS_ON -> [fetchApi] // @DATA_CONTRACT params -> { taskId } // @DATA_CONTRACT response -> { id, plugin_id, status, started_at, completed_at, progress?, error?, retry_count?, params?, result?, input_request?: { type, databases?[] } } getTask: (taskId: string) => fetchApi(`/tasks/${taskId}`), // #endregion getTask // #region getTaskLogs [C:2] [TYPE Function] [SEMANTICS tasks,api,logs] // @BRIEF Fetch paginated task log entries with level/source/search filters. // @LAYER API // @RELATION DEPENDS_ON -> [fetchApi] // @DATA_CONTRACT params -> { taskId, level?, source?, search?, offset?, limit? } // @DATA_CONTRACT response -> { logs: { timestamp, level, source, message }[], total: number } getTaskLogs: (taskId: string, options: TaskLogQueryOptions = {}) => { const params = new URLSearchParams(); if (options.level) params.append('level', options.level); if (options.source) params.append('source', options.source); if (options.search) params.append('search', options.search); if (options.offset != null) params.append('offset', String(options.offset)); if (options.limit != null) params.append('limit', String(options.limit)); return fetchApi(`/tasks/${taskId}/logs${params.toString() ? `?${params.toString()}` : ''}`); }, // #endregion getTaskLogs // #region createTask [C:2] [TYPE Function] [SEMANTICS tasks,api,create] // @BRIEF Create a new background task with plugin ID and params. // @LAYER API // @RELATION DEPENDS_ON -> [postApi] // @DATA_CONTRACT params -> { plugin_id, params: object } // @DATA_CONTRACT response -> { task_id: string } createTask: (pluginId: string, params: unknown, requestOptions?: FetchOptions) => postApi('/tasks', { plugin_id: pluginId, params }, requestOptions), // #endregion createTask // ═══ Profile ══════════════════════════════════════════════════ // #region getProfilePreferences [C:2] [TYPE Function] [SEMANTICS profile,api,preferences] // @BRIEF Fetch current user profile preferences. // @LAYER API // @RELATION DEPENDS_ON -> [fetchApi] // @DATA_CONTRACT response -> { auto_open_task_drawer?: boolean, default_environment_id?: string, default_profile_filter?: object } getProfilePreferences: () => fetchApi('/profile/preferences'), // #endregion getProfilePreferences // #region updateProfilePreferences [C:2] [TYPE Function] [SEMANTICS profile,api,update] // @BRIEF Update user profile preferences (PATCH). // @LAYER API // @RELATION DEPENDS_ON -> [requestApi] // @DATA_CONTRACT params -> { auto_open_task_drawer?, default_environment_id?, default_profile_filter? } // @DATA_CONTRACT response -> { success: boolean } updateProfilePreferences: (payload: unknown) => requestApi('/profile/preferences', 'PATCH', payload), // #endregion updateProfilePreferences // #region lookupSupersetAccounts [C:2] [TYPE Function] [SEMANTICS profile,api,superset,lookup] // @BRIEF Search Superset accounts for profile identity mapping. // @LAYER API // @RELATION DEPENDS_ON -> [fetchApi] // @DATA_CONTRACT params -> { environmentId, search?, page_index?, page_size?, sort_column?, sort_order? } // @DATA_CONTRACT response -> { accounts: { id, username, first_name?, last_name?, email? }[], total: number } lookupSupersetAccounts: (environmentId: string, options: SupersetAccountOptions = {}) => { const eid = String(environmentId || '').trim(); if (!eid) throw new Error('environmentId is required for Superset account lookup'); const params = new URLSearchParams({ environment_id: eid }); if (options.search) params.append('search', options.search); if (options.page_index != null) params.append('page_index', String(options.page_index)); if (options.page_size != null) params.append('page_size', String(options.page_size)); if (options.sort_column) params.append('sort_column', options.sort_column); if (options.sort_order) params.append('sort_order', options.sort_order); return fetchApi(`/profile/superset-accounts?${params.toString()}`); }, // #endregion lookupSupersetAccounts // ═══ Settings ═════════════════════════════════════════════════ // #region getSettings [C:2] [TYPE Function] [SEMANTICS settings,api,list] // @BRIEF Fetch all settings (environments, storage, logging). // @LAYER API // @RELATION DEPENDS_ON -> [fetchApi] // @DATA_CONTRACT response -> { environments, storage, logging, llm, migration, features, system } getSettings: () => fetchApi('/settings'), // #endregion getSettings // #region updateGlobalSettings [C:2] [TYPE Function] [SEMANTICS settings,api,update,global] // @BRIEF Update global settings (PATCH). // @LAYER API // @RELATION DEPENDS_ON -> [requestApi] updateGlobalSettings: (s: unknown) => requestApi('/settings/global', 'PATCH', s), // #endregion updateGlobalSettings // #region getEnvironments [C:2] [TYPE Function] [SEMANTICS settings,api,environments,list] // @BRIEF Fetch all configured Superset environments. // @LAYER API // @RELATION DEPENDS_ON -> [fetchApi] // @DATA_CONTRACT response -> { environments: { id, name, url, database?, schedule? }[] } getEnvironments: () => fetchApi('/settings/environments'), // #endregion getEnvironments // #region addEnvironment [C:2] [TYPE Function] [SEMANTICS settings,api,environments,create] // @BRIEF Add a new Superset environment. // @LAYER API // @RELATION DEPENDS_ON -> [postApi] // @DATA_CONTRACT params -> { name, url, database?, schedule? } // @DATA_CONTRACT response -> { id, name, url } addEnvironment: (env: unknown) => postApi('/settings/environments', env), // #endregion addEnvironment // #region updateEnvironment [C:2] [TYPE Function] [SEMANTICS settings,api,environments,update] // @BRIEF Full update of an environment by ID (PUT). // @LAYER API // @RELATION DEPENDS_ON -> [requestApi] updateEnvironment: (id: string, env: unknown) => requestApi(`/settings/environments/${id}`, 'PUT', env), // #endregion updateEnvironment // #region gitReleasePolicy [C:2] [TYPE Function] [SEMANTICS git,release,policy,settings] // @BRIEF Read and update the approval policy for Git release candidates. getGitReleasePolicy: () => requestApi('/settings/git-release-policy', 'GET'), updateGitReleasePolicy: (policy: unknown) => requestApi('/settings/git-release-policy', 'PUT', policy), // #endregion gitReleasePolicy // #region deleteEnvironment [C:2] [TYPE Function] [SEMANTICS settings,api,environments,delete] // @BRIEF Delete an environment by ID. // @LAYER API // @RELATION DEPENDS_ON -> [requestApi] deleteEnvironment: (id: string) => requestApi(`/settings/environments/${id}`, 'DELETE'), // #endregion deleteEnvironment // #region testEnvironmentConnection [C:2] [TYPE Function] [SEMANTICS settings,api,environments,test] // @BRIEF Test connection to an environment. // @LAYER API // @RELATION DEPENDS_ON -> [postApi] // @DATA_CONTRACT response -> { success: boolean, message?: string } testEnvironmentConnection: (id: string) => postApi(`/settings/environments/${id}/test`, {}), // #endregion testEnvironmentConnection // #region updateEnvironmentSchedule [C:2] [TYPE Function] [SEMANTICS settings,api,environments,schedule] // @BRIEF Update environment refresh schedule (PUT). // @LAYER API // @RELATION DEPENDS_ON -> [requestApi] updateEnvironmentSchedule: (id: string, s: unknown, options?: FetchOptions) => requestApi(`/environments/${id}/schedule`, 'PUT', s, options), // #endregion updateEnvironmentSchedule // #region getStorageSettings [C:2] [TYPE Function] [SEMANTICS settings,api,storage,list] // @BRIEF Fetch storage configuration. // @LAYER API // @RELATION DEPENDS_ON -> [fetchApi] // @DATA_CONTRACT response -> { path, max_size?, allowed_types? } getStorageSettings: () => fetchApi('/settings/storage'), // #endregion getStorageSettings // #region updateStorageSettings [C:2] [TYPE Function] [SEMANTICS settings,api,storage,update] // @BRIEF Update storage configuration (PUT). // @LAYER API // @RELATION DEPENDS_ON -> [requestApi] updateStorageSettings: (s: unknown) => requestApi('/settings/storage', 'PUT', s), // #endregion updateStorageSettings // #region getEnvironmentsList [C:2] [TYPE Function] [SEMANTICS settings,api,environments,list,flat] // @BRIEF Fetch flat list of environments (simplified form). // @LAYER API // @RELATION DEPENDS_ON -> [fetchApi] // @DATA_CONTRACT response -> { id, name, url, status }[] // flat list form getEnvironmentsList: (options?: FetchOptions) => fetchApi('/environments', options), // #endregion getEnvironmentsList // ═══ LLM ══════════════════════════════════════════════════════ // #region getLlmStatus [C:2] [TYPE Function] [SEMANTICS llm,api,status] // @BRIEF Fetch LLM service status and provider health. // @LAYER API // @RELATION DEPENDS_ON -> [fetchApi] // @DATA_CONTRACT response -> { configured: boolean, providers: { id, name, status }[] } getLlmStatus: () => fetchApi('/llm/status'), // #endregion getLlmStatus // #region fetchLlmModels [C:2] [TYPE Function] [SEMANTICS llm,api,providers,fetch-models] // @BRIEF Fetch available models from an LLM provider. // @LAYER API // @RELATION DEPENDS_ON -> [postApi] fetchLlmModels: (p: unknown) => postApi('/llm/providers/fetch-models', p), // #endregion fetchLlmModels // #region getEnvironmentDatabases [C:2] [TYPE Function] [SEMANTICS environments,api,databases] // @BRIEF Fetch databases for an environment (used for mapping). // @LAYER API // @RELATION DEPENDS_ON -> [fetchApi] // @DATA_CONTRACT params -> { env_id } // @DATA_CONTRACT response -> { databases: { uuid, database_name, backend? }[] } getEnvironmentDatabases: (id: string) => fetchApi(`/environments/${id}/databases`), // #endregion getEnvironmentDatabases // ═══ Storage ══════════════════════════════════════════════════ // #region getStorageFileBlob [C:2] [TYPE Function] [SEMANTICS storage,api,file,blob] // @BRIEF Download a storage file as a Blob. // @LAYER API // @RELATION DEPENDS_ON -> [fetchApiBlob] // @DATA_CONTRACT params -> { path } // @DATA_CONTRACT response -> Blob (binary file) getStorageFileBlob: (path: string) => fetchApiBlob(`/storage/file?path=${encodeURIComponent(path)}`), // #endregion getStorageFileBlob // #region getStorageFiles [C:2] [TYPE Function] [SEMANTICS storage,api,files,list] // @BRIEF List files in a storage category, with optional subpath. // @LAYER API // @RELATION DEPENDS_ON -> [fetchApi] // @DATA_CONTRACT params -> { category, path? } // @DATA_CONTRACT response -> StoredFile[] getStorageFiles: (category: string, subpath?: string, options?: FetchOptions) => { const query = subpath ? `/storage/files?category=${encodeURIComponent(category)}&path=${encodeURIComponent(subpath)}` : `/storage/files?category=${encodeURIComponent(category)}`; return fetchApi(query, options); }, // #endregion getStorageFiles // ═══ Dashboards ═══════════════════════════════════════════════ // #region getDashboards [C:2] [TYPE Function] [SEMANTICS dashboards,api,list,pagination] // @BRIEF Fetch paginated dashboards for an environment with filters and profile context. // @LAYER API // @RELATION DEPENDS_ON -> [fetchApi] // @DATA_CONTRACT params -> { env_id, page?, page_size?, page_context?, apply_profile_default?, override_show_all?, search?, filters?: { title?, git_status?, llm_status?, changed_on?, actor? } } // @DATA_CONTRACT response -> { dashboards: { id, title, slug, last_modified, owners, git_status, last_task }[], total: number, page: number, page_size: number, total_pages: number, effective_profile_filter?: { applied: boolean, override_show_all: boolean, username?: string, match_logic?: string } } getDashboards: (envId: string, options: DashboardListParams = {}) => { const params = new URLSearchParams({ env_id: envId }); if (options.search) params.append('search', options.search); if (options.page) params.append('page', options.page); if (options.page_size) params.append('page_size', options.page_size); if (options.page_context) params.append('page_context', options.page_context); if (options.apply_profile_default != null) params.append('apply_profile_default', String(Boolean(options.apply_profile_default))); if (options.override_show_all != null) params.append('override_show_all', String(Boolean(options.override_show_all))); if (options.filters?.title) for (const v of options.filters.title) params.append('filter_title', v); if (options.filters?.git_status) for (const v of options.filters.git_status) params.append('filter_git_status', v); if (options.filters?.llm_status) for (const v of options.filters.llm_status) params.append('filter_llm_status', v); if (options.filters?.changed_on) for (const v of options.filters.changed_on) params.append('filter_changed_on', v); if (options.filters?.actor) for (const v of options.filters.actor) params.append('filter_actor', v); if (options.filter_changed_on_from) params.append('filter_changed_on_from', options.filter_changed_on_from); if (options.filter_changed_on_to) params.append('filter_changed_on_to', options.filter_changed_on_to); return fetchApi(`/dashboards?${params.toString()}`); }, // #endregion getDashboards // #region getDashboardDetail [C:2] [TYPE Function] [SEMANTICS dashboards,api,detail] // @BRIEF Fetch a single dashboard by ref (ID or slug). // @LAYER API // @RELATION DEPENDS_ON -> [fetchApi] // @DATA_CONTRACT params -> { env_id, ref: dashboard_id_or_slug } // @DATA_CONTRACT response -> { id, title, slug, last_modified, status, chart_count?, dataset_count?, owner_ids?, tags?, metadata_json? } getDashboardDetail: (envId: string, ref: string) => fetchApi(`/dashboards/${encodeURIComponent(String(ref))}?env_id=${envId}`), // #endregion getDashboardDetail // #region getDashboardTaskHistory [C:2] [TYPE Function] [SEMANTICS dashboards,api,tasks,history] // @BRIEF Fetch task history for a specific dashboard. // @LAYER API // @RELATION DEPENDS_ON -> [fetchApi] // @DATA_CONTRACT params -> { env_id, ref, opts?: { limit? } } // @DATA_CONTRACT response -> { tasks: { id, plugin_id, status, started_at, completed_at, error?, params? }[] } getDashboardTaskHistory: (envId: string, ref: string, opts: { limit?: number } = {}) => { const params = new URLSearchParams(); if (envId) params.append('env_id', envId); if (opts.limit) params.append('limit', opts.limit); return fetchApi(`/dashboards/${encodeURIComponent(String(ref))}/tasks?${params.toString()}`); }, // #endregion getDashboardTaskHistory // #region getDashboardThumbnail [C:2] [TYPE Function] [SEMANTICS dashboards,api,thumbnail,blob] // @BRIEF Fetch dashboard thumbnail as Blob with optional force regeneration. // @LAYER API // @RELATION DEPENDS_ON -> [fetchApiBlob] // @DATA_CONTRACT params -> { env_id, ref, opts?: { force? } } // @DATA_CONTRACT response -> Blob (image/png thumbnail) getDashboardThumbnail: (envId: string, ref: string, opts: DashboardThumbnailOptions = {}) => { const params = new URLSearchParams({ env_id: envId }); if (opts.force != null) params.append('force', String(Boolean(opts.force))); return fetchApiBlob(`/dashboards/${encodeURIComponent(String(ref))}/thumbnail?${params.toString()}`, { notifyError: false }); }, // #endregion getDashboardThumbnail // #region getDatabaseMappings [C:2] [TYPE Function] [SEMANTICS dashboards,api,mappings,database] // @BRIEF Fetch database mappings between source and target environments. // @LAYER API // @RELATION DEPENDS_ON -> [fetchApi] // @DATA_CONTRACT params -> { src: source_env_id, tgt: target_env_id } // @DATA_CONTRACT response -> { mappings: { source_db_uuid, target_db_uuid, source_db_name, target_db_name, confidence? }[] } getDatabaseMappings: (src: string, tgt: string) => fetchApi(`/dashboards/db-mappings?source_env_id=${src}&target_env_id=${tgt}`), // #endregion getDatabaseMappings // #region calculateMigrationDryRun [C:2] [TYPE Function] [SEMANTICS migration,api,dry-run] // @BRIEF POST dry-run calculation for dashboard migration preview. // @LAYER API // @RELATION DEPENDS_ON -> [postApi] // @DATA_CONTRACT params -> { source_env_id, target_env_id, selected_dashboard_ids: number[], replace_db_config?, fix_cross_filters? } // @DATA_CONTRACT response -> { diff: { dashboards, charts, datasets }, summary: { dashboards, charts, datasets }, risk: { score, level, items[] }, selected_dashboard_titles[] } calculateMigrationDryRun: (p: unknown) => postApi('/migration/dry-run', p), // #endregion calculateMigrationDryRun // ═══ Datasets ═════════════════════════════════════════════════ // #region getDatasets [C:2] [TYPE Function] [SEMANTICS datasets,api,list,pagination] // @BRIEF Fetch paginated datasets for an environment. // @LAYER API // @RELATION DEPENDS_ON -> [fetchApi] // @DATA_CONTRACT params -> { env_id, search?, filter?, page?, page_size? } // @DATA_CONTRACT response -> { datasets: { id, table_name, schema, database, mapped_fields?: { total, mapped }, metric_count?, last_task? }[], stats?: object, total: number, page: number, total_pages: number } getDatasets: (envId: string, opts: DatasetQueryOptions = {}) => { const params = new URLSearchParams({ env_id: envId }); if (opts.search) params.append('search', opts.search); if (opts.filter) params.append('filter', opts.filter); if (opts.page) params.append('page', opts.page); if (opts.page_size) params.append('page_size', opts.page_size); return fetchApi(`/datasets?${params.toString()}`); }, // #endregion getDatasets // #region getDatasetIds [C:2] [TYPE Function] [SEMANTICS datasets,api,ids,lookup] // @BRIEF Fetch dataset IDs with optional search (lightweight lookup). // @LAYER API // @RELATION DEPENDS_ON -> [fetchApi] // @DATA_CONTRACT params -> { env_id, search? } // @DATA_CONTRACT response -> { ids: { id, table_name, schema, database }[] } getDatasetIds: (envId: string, opts: { search?: string } = {}) => { const params = new URLSearchParams({ env_id: envId }); if (opts.search) params.append('search', opts.search); return fetchApi(`/datasets/ids?${params.toString()}`); }, // #endregion getDatasetIds // #region getDatasetDetail [C:2] [TYPE Function] [SEMANTICS datasets,api,detail] // @BRIEF Fetch a single dataset detail with columns and metrics. // @LAYER API // @RELATION DEPENDS_ON -> [fetchApi] // @DATA_CONTRACT params -> { env_id, datasetId } // @DATA_CONTRACT response -> { id, table_name, schema, database, columns?: { name, type }[], metrics?: { name, expression }[], mapped_fields?: object } getDatasetDetail: (envId: string, datasetId: string) => fetchApi(`/datasets/${datasetId}?env_id=${envId}`), // #endregion getDatasetDetail // ═══ Consolidated Settings ════════════════════════════════════ // #region getConsolidatedSettings [C:2] [TYPE Function] [SEMANTICS settings,api,consolidated,list] // @BRIEF Fetch all settings in one consolidated response. // @LAYER API // @RELATION DEPENDS_ON -> [fetchApi] // @DATA_CONTRACT response -> { environments, storage, logging, llm, migration, features, system } getConsolidatedSettings: () => fetchApi('/settings/consolidated'), // #endregion getConsolidatedSettings // #region getAllowedLanguages [C:1] [TYPE Function] [SEMANTICS settings,api,languages,allowed] // @BRIEF Fetch the list of allowed BCP-47 language codes (public, no auth required). // @LAYER API // @RELATION DEPENDS_ON -> [fetchApi] // @DATA_CONTRACT response -> string[] getAllowedLanguages: () => fetchApi('/settings/allowed-languages'), // #endregion getAllowedLanguages // #region updateConsolidatedSettings [C:2] [TYPE Function] [SEMANTICS settings,api,consolidated,update] // @BRIEF Update consolidated settings (PATCH). // @LAYER API // @RELATION DEPENDS_ON -> [requestApi] // @DATA_CONTRACT params -> { environments?, storage?, logging?, llm?, migration?, features?, system? } // @DATA_CONTRACT response -> { success: boolean } updateConsolidatedSettings: (s: unknown) => requestApi('/settings/consolidated', 'PATCH', s), // #endregion updateConsolidatedSettings // ═══ Connections ═══════════════════════════════════════════════ // #region fetchConnections [C:1] [TYPE Function] [SEMANTICS settings,api,connections,list] // @BRIEF Fetch all database connections with masked passwords. // @LAYER API // @RELATION DEPENDS_ON -> [fetchApi] // @DATA_CONTRACT response -> [{ id, name, host, port, database, username, dialect, pool_size, extra_params, created_at, updated_at, used_by }] fetchConnections: () => fetchApi('/settings/connections'), // #endregion fetchConnections // #region getConnection [C:1] [TYPE Function] [SEMANTICS settings,api,connections,get] // @BRIEF Fetch a single database connection by ID. // @LAYER API // @RELATION DEPENDS_ON -> [fetchApi] getConnection: (id: string) => fetchApi(`/settings/connections/${id}`), // #endregion getConnection // #region createConnection [C:1] [TYPE Function] [SEMANTICS settings,api,connections,create] // @BRIEF Create a new database connection. // @LAYER API // @RELATION DEPENDS_ON -> [postApi] // @DATA_CONTRACT body -> { name, host, port, database, username, password, dialect, extra_params?, pool_size? } // @DATA_CONTRACT response -> { id, name, host, port, database, username, dialect, ... } (password masked) createConnection: (data: unknown) => postApi('/settings/connections', data), // #endregion createConnection // #region updateConnection [C:1] [TYPE Function] [SEMANTICS settings,api,connections,update] // @BRIEF Update a database connection by ID. Empty/masked password = keep existing. // @LAYER API // @RELATION DEPENDS_ON -> [requestApi] updateConnection: (id: string, data: unknown) => requestApi(`/settings/connections/${id}`, 'PUT', data), // #endregion updateConnection // #region deleteConnection [C:1] [TYPE Function] [SEMANTICS settings,api,connections,delete] // @BRIEF Delete a database connection by ID. Blocked if referenced by active jobs. // @LAYER API // @RELATION DEPENDS_ON -> [requestApi] // @DATA_CONTRACT response -> { message: "Connection deleted" } or 409 { detail: { blocking_jobs: string[] } } deleteConnection: (id: string) => requestApi(`/settings/connections/${id}`, 'DELETE'), // #endregion deleteConnection // #region testConnection [C:1] [TYPE Function] [SEMANTICS settings,api,connections,test] // @BRIEF Test connectivity to a database connection. Runs SELECT 1. // @LAYER API // @RELATION DEPENDS_ON -> [postApi] // @DATA_CONTRACT response -> { success: bool, latency_ms?: int, db_version?: string, error?: string } testConnection: (id: string) => postApi(`/settings/connections/${id}/test`, {}), // #endregion testConnection // ═══ Automation ═══════════════════════════════════════════════ // #region getValidationPolicies [C:2] [TYPE Function] [SEMANTICS automation,api,policies,list] // @BRIEF Fetch all validation automation policies. // @LAYER API // @RELATION DEPENDS_ON -> [fetchApi] // @DATA_CONTRACT response -> { policies: { id, name, is_active, schedule?, environment_id, dashboard_ids?, task_type? }[] } getValidationPolicies: () => fetchApi('/settings/automation/policies'), // #endregion getValidationPolicies // #region createValidationPolicy [C:2] [TYPE Function] [SEMANTICS automation,api,policies,create] // @BRIEF Create a new validation automation policy. // @LAYER API // @RELATION DEPENDS_ON -> [postApi] createValidationPolicy: (p: unknown) => postApi('/settings/automation/policies', p), // #endregion createValidationPolicy // #region updateValidationPolicy [C:2] [TYPE Function] [SEMANTICS automation,api,policies,update] // @BRIEF Update a validation policy by ID (PATCH). // @LAYER API // @RELATION DEPENDS_ON -> [requestApi] updateValidationPolicy: (id: string, p: unknown) => requestApi(`/settings/automation/policies/${id}`, 'PATCH', p), // #endregion updateValidationPolicy // #region deleteValidationPolicy [C:2] [TYPE Function] [SEMANTICS automation,api,policies,delete] // @BRIEF Delete a validation policy by ID. // @LAYER API // @RELATION DEPENDS_ON -> [requestApi] deleteValidationPolicy: (id: string) => requestApi(`/settings/automation/policies/${id}`, 'DELETE'), // #endregion deleteValidationPolicy // #region getTranslationSchedules [C:2] [TYPE Function] [SEMANTICS automation,api,translation,schedules] // @BRIEF Fetch all translation automation schedules. // @LAYER API // @RELATION DEPENDS_ON -> [fetchApi] // @DATA_CONTRACT response -> { schedules: { id, name, config_id?, cron_expression?, is_active }[] } getTranslationSchedules: () => fetchApi('/settings/automation/translation-schedules'), // #endregion getTranslationSchedules // ═══ Health ═══════════════════════════════════════════════════ // #region getHealthSummary [C:2] [TYPE Function] [SEMANTICS health,api,summary] // @BRIEF Fetch dashboard health summary, optionally scoped to environment. // @LAYER API // @RELATION DEPENDS_ON -> [fetchApi] // @DATA_CONTRACT params -> { environmentId? } // @DATA_CONTRACT response -> { summary: { total_dashboards, failing_dashboards, total_datasets, environments }[], items?: { dashboard_id, dashboard_slug, title, last_validation_status, last_validation_run_at, failing_count }[] } getHealthSummary: (environmentId?: string) => { const query = environmentId ? `?env_id=${encodeURIComponent(environmentId)}` : ''; return fetchApi(`/health/summary${query}`, { suppressToast: true }); }, // #endregion getHealthSummary // ═══ LLM Providers ════════════════════════════════════════════ // #region getLlmProviders [C:2] [TYPE Function] [SEMANTICS llm,api,providers,list] // @BRIEF Fetch all configured LLM providers. // @LAYER API // @RELATION DEPENDS_ON -> [fetchApi] // @DATA_CONTRACT response -> { providers: { id, name, type, status }[] } getLlmProviders: () => fetchApi('/llm/providers'), // #endregion getLlmProviders // ═══ Validation Tasks ═════════════════════════════════════════ // #region parseValidationUrl [C:2] [TYPE Function] [SEMANTICS validation,api,url,parse] // @BRIEF Parse a Superset dashboard URL into validation task params. // @LAYER API // @RELATION DEPENDS_ON -> [postApi] // @DATA_CONTRACT params -> { url, environment_id } // @DATA_CONTRACT response -> { dashboard_id, title?, environment_id, charts?[] } parseValidationUrl: (url: string, envId: string) => postApi('/validation-tasks/parse-url', { url, environment_id: envId }), // #endregion parseValidationUrl // #region getValidationTasks [C:2] [TYPE Function] [SEMANTICS validation,api,tasks,list,pagination] // @BRIEF Fetch paginated validation tasks with status/environment/search filters. // @LAYER API // @RELATION DEPENDS_ON -> [fetchApi] // @DATA_CONTRACT params -> { page?, page_size?, is_active?, environment_id?, search? } // @DATA_CONTRACT response -> { tasks: { id, name, environment_id, is_active, schedule?, last_run_at?, last_run_status?, dashboard_ids? }[], total: number, page: number, page_size: number } getValidationTasks: (params: ValidationTaskQueryParams = {}) => { const qs = new URLSearchParams(); if (params.page != null) qs.append('page', String(params.page)); if (params.page_size != null) qs.append('page_size', String(params.page_size)); if (params.is_active != null) qs.append('is_active', String(Boolean(params.is_active))); if (params.environment_id) qs.append('environment_id', params.environment_id); if (params.search) qs.append('search', params.search); const query = qs.toString(); return fetchApi(`/validation-tasks${query ? `?${query}` : ''}`); }, // #endregion getValidationTasks // #region createValidationTask [C:2] [TYPE Function] [SEMANTICS validation,api,tasks,create] // @BRIEF Create a new validation task with schedule and dashboard scope. // @LAYER API // @RELATION DEPENDS_ON -> [postApi] // @DATA_CONTRACT params -> { name, environment_id, dashboard_ids: number[], schedule?, llm_provider?, validation_type? } // @DATA_CONTRACT response -> { id, name } createValidationTask: (data: unknown) => postApi('/validation-tasks', data), // #endregion createValidationTask // #region getValidationTask [C:2] [TYPE Function] [SEMANTICS validation,api,tasks,detail] // @BRIEF Fetch a single validation task by ID with full config. // @LAYER API // @RELATION DEPENDS_ON -> [fetchApi] // @DATA_CONTRACT params -> { id } // @DATA_CONTRACT response -> { id, name, environment_id, is_active, schedule?, params?, dashboard_ids?, last_run?, runs_count? } getValidationTask: (id: string) => fetchApi(`/validation-tasks/${id}`), // #endregion getValidationTask // #region updateValidationTask [C:2] [TYPE Function] [SEMANTICS validation,api,tasks,update] // @BRIEF Update a validation task by ID (PUT). // @LAYER API // @RELATION DEPENDS_ON -> [requestApi] updateValidationTask: (id: string, data: unknown) => requestApi(`/validation-tasks/${id}`, 'PUT', data), // #endregion updateValidationTask // #region deleteValidationTask [C:2] [TYPE Function] [SEMANTICS validation,api,tasks,delete] // @BRIEF Delete a validation task and optionally its runs. // @LAYER API // @RELATION DEPENDS_ON -> [deleteApi] // @DATA_CONTRACT params -> { id, deleteRuns?: boolean } deleteValidationTask: (id: string, deleteRuns: boolean = true) => { const qs = deleteRuns ? '?delete_runs=true' : ''; return deleteApi(`/validation-tasks/${id}${qs}`); }, // #endregion deleteValidationTask // #region triggerValidationRun [C:2] [TYPE Function] [SEMANTICS validation,api,run,trigger] // @BRIEF Trigger a new validation run for a task. // @LAYER API // @RELATION DEPENDS_ON -> [postApi] // @DATA_CONTRACT params -> { id: policy_id } // @DATA_CONTRACT response -> { run_id: string, status: string } triggerValidationRun: (id: string) => postApi(`/validation-tasks/${id}/run`, {}), // #endregion triggerValidationRun // #region toggleValidationTaskStatus [C:2] [TYPE Function] [SEMANTICS validation,api,tasks,toggle] // @BRIEF Toggle active/inactive status of a validation task (PATCH). // @LAYER API // @RELATION DEPENDS_ON -> [requestApi] // @DATA_CONTRACT params -> { id, isActive: boolean } toggleValidationTaskStatus: (id: string, isActive: boolean) => requestApi(`/validation-tasks/${id}/status`, 'PATCH', { is_active: isActive }), // #endregion toggleValidationTaskStatus // #region getValidationRuns [C:2] [TYPE Function] [SEMANTICS validation,api,runs,list,pagination] // @BRIEF Fetch paginated validation runs for a task. // @LAYER API // @RELATION DEPENDS_ON -> [fetchApi] // @DATA_CONTRACT params -> { taskId, page?, page_size? } // @DATA_CONTRACT response -> { runs: { id, status, started_at, completed_at, summary?, error? }[], total: number, page: number } getValidationRuns: (taskId: string, params: ValidationRunQueryParams = {}) => { const qs = new URLSearchParams(); if (params.page != null) qs.append('page', String(params.page)); if (params.page_size != null) qs.append('page_size', String(params.page_size)); const query = qs.toString(); return fetchApi(`/validation-tasks/${taskId}/runs${query ? `?${query}` : ''}`); }, // #endregion getValidationRuns // #region getValidationRunDetail [C:2] [TYPE Function] [SEMANTICS validation,api,runs,detail] // @BRIEF Fetch a single validation run detail with results and logs. // @LAYER API // @RELATION DEPENDS_ON -> [fetchApi] // @DATA_CONTRACT params -> { taskId, runId } // @DATA_CONTRACT response -> { id, task_id, status, started_at, completed_at, result?: { items?: { dashboard_id, dashboard_title, chart_id?, status, issues? }[] }, error?, logs? } getValidationRunDetail: (taskId: string, runId: string) => fetchApi(`/validation-tasks/${taskId}/runs/${runId}`), // #endregion getValidationRunDetail // #region getValidationStatusBatch [C:2] [TYPE Function] [SEMANTICS validation,api,status,batch] // @BRIEF Batch-fetch latest validation status for multiple dashboard IDs. // @LAYER API // @RELATION DEPENDS_ON -> [fetchApi] // @DATA_CONTRACT params -> { env_id, dashboard_ids: comma-separated string } // @DATA_CONTRACT response -> { [dashboard_id]: { status: PASS|FAIL|WARN, last_run_at?, task_name?, run_id?, history?: { status, last_run_at, task_name }[] } } getValidationStatusBatch: (envId: string, dashboardIds: string) => fetchApi(`/validation-tasks/status/batch?env_id=${encodeURIComponent(envId)}&dashboard_ids=${encodeURIComponent(dashboardIds)}`), // #endregion getValidationStatusBatch // ═══ API Keys ═════════════════════════════════════════════════ // #region listApiKeys [C:2] [TYPE Function] [SEMANTICS admin,api,api-keys,list] // @BRIEF List all API keys (admin only). // @LAYER API // @RELATION DEPENDS_ON -> [fetchApi] // @DATA_CONTRACT response -> { api_keys: { id, name, key_prefix, created_at, last_used_at?, is_active }[] } listApiKeys: () => fetchApi('/admin/api-keys/'), // #endregion listApiKeys // #region createApiKey [C:2] [TYPE Function] [SEMANTICS admin,api,api-keys,create] // @BRIEF Create a new API key, returns the secret once (admin only). // @LAYER API // @RELATION DEPENDS_ON -> [postApi] // @DATA_CONTRACT params -> { name, scopes?[] } // @DATA_CONTRACT response -> { id, name, key: string, key_prefix, created_at } createApiKey: (payload: unknown) => postApi('/admin/api-keys/', payload, { suppressToast: true }), // #endregion createApiKey // #region revokeApiKey [C:2] [TYPE Function] [SEMANTICS admin,api,api-keys,delete] // @BRIEF Revoke an API key (admin only). // @LAYER API // @RELATION DEPENDS_ON -> [requestApi] revokeApiKey: (keyId: string) => requestApi(`/admin/api-keys/${keyId}`, 'DELETE'), // #endregion revokeApiKey // #region getEncryptionHealth [C:2] [TYPE Function] [SEMANTICS security,encryption,health] // @BRIEF Inventory all stored encrypted secrets and report broken ones. getEncryptionHealth: () => fetchApi('/security/encryption/health'), // #endregion getEncryptionHealth // #region recoverEncryptedSecrets [C:2] [TYPE Function] [SEMANTICS security,encryption,recover] // @BRIEF Submit replacement secrets for items that could not be decrypted. recoverEncryptedSecrets: (payload: unknown) => postApi('/security/encryption/recover', payload), // #endregion recoverEncryptedSecrets }; // #endregion ApiRegistry // #endregion ApiModule export { fetchApi, postApi, deleteApi, requestApi }; export const getPlugins = api.getPlugins; export const getTasks = api.getTasks; export const getTask = api.getTask; export const createTask = api.createTask; export const getProfilePreferences = api.getProfilePreferences; export const updateProfilePreferences = api.updateProfilePreferences; export const lookupSupersetAccounts = api.lookupSupersetAccounts; export const getSettings = api.getSettings; export const updateGlobalSettings = api.updateGlobalSettings; export const getEnvironments = api.getEnvironments; export const addEnvironment = api.addEnvironment; export const updateEnvironment = api.updateEnvironment; export const deleteEnvironment = api.deleteEnvironment; export const testEnvironmentConnection = api.testEnvironmentConnection; export const updateEnvironmentSchedule = api.updateEnvironmentSchedule; export const getEnvironmentsList = api.getEnvironmentsList; export const getStorageSettings = api.getStorageSettings; export const updateStorageSettings = api.updateStorageSettings; export const getDashboards = api.getDashboards; export const getDatasets = api.getDatasets; export const getConsolidatedSettings = api.getConsolidatedSettings; export const getAllowedLanguages = api.getAllowedLanguages; export const updateConsolidatedSettings = api.updateConsolidatedSettings; export const getValidationPolicies = api.getValidationPolicies; export const createValidationPolicy = api.createValidationPolicy; export const updateValidationPolicy = api.updateValidationPolicy; export const deleteValidationPolicy = api.deleteValidationPolicy; export const getTranslationSchedules = api.getTranslationSchedules; export const getHealthSummary = api.getHealthSummary; export const getValidationTasks = api.getValidationTasks; export const getValidationTask = api.getValidationTask; export const createValidationTask = api.createValidationTask; export const updateValidationTask = api.updateValidationTask; export const deleteValidationTask = api.deleteValidationTask; export const triggerValidationRun = api.triggerValidationRun; export const toggleValidationTaskStatus = api.toggleValidationTaskStatus; export const getValidationRuns = api.getValidationRuns; export const getValidationRunDetail = api.getValidationRunDetail; export const fetchConnections = api.fetchConnections; export const getConnection = api.getConnection; export const createConnection = api.createConnection; export const updateConnection = api.updateConnection; export const deleteConnection = api.deleteConnection; export const testConnection = api.testConnection; export const getEncryptionHealth = api.getEncryptionHealth; export const recoverEncryptedSecrets = api.recoverEncryptedSecrets;