// [DEF:api_module:Module] // @TIER: STANDARD // @SEMANTICS: api, client, fetch, rest // @PURPOSE: Handles all communication with the backend API. // @LAYER: Infra-API import { addToast } from './toasts.js'; import { PUBLIC_WS_URL } from '$env/static/public'; const API_BASE_URL = '/api'; // [DEF:getWsUrl:Function] // @PURPOSE: Returns the WebSocket URL for a specific task, with fallback logic. // @PRE: taskId is provided. // @POST: Returns valid WebSocket URL string. // @PARAM: taskId (string) - The ID of the task. // @RETURN: string - The WebSocket URL. export const getWsUrl = (taskId) => { let baseUrl = PUBLIC_WS_URL; if (!baseUrl) { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; // Use the current host and port to allow Vite proxy to handle the connection baseUrl = `${protocol}//${window.location.host}`; } return `${baseUrl}/ws/logs/${taskId}`; }; // [/DEF:getWsUrl:Function] // [DEF:getAuthHeaders:Function] // @PURPOSE: Returns headers with Authorization if token exists. function getAuthHeaders() { const headers = { 'Content-Type': 'application/json', }; if (typeof window !== 'undefined') { const token = localStorage.getItem('auth_token'); if (token) { headers['Authorization'] = `Bearer ${token}`; } } return headers; } // [/DEF:getAuthHeaders:Function] // [DEF:fetchApi:Function] // @PURPOSE: Generic GET request wrapper. // @PRE: endpoint string is provided. // @POST: Returns Promise resolving to JSON data or throws on error. // @PARAM: endpoint (string) - API endpoint. // @RETURN: Promise - JSON response. async function fetchApi(endpoint) { try { console.log(`[api.fetchApi][Action] Fetching from context={{'endpoint': '${endpoint}'}}`); const response = await fetch(`${API_BASE_URL}${endpoint}`, { headers: getAuthHeaders() }); console.log(`[api.fetchApi][Action] Received response context={{'status': ${response.status}, 'ok': ${response.ok}}}`); if (!response.ok) { const errorData = await response.json().catch(() => ({})); const message = errorData.detail ? (typeof errorData.detail === 'string' ? errorData.detail : JSON.stringify(errorData.detail)) : `API request failed with status ${response.status}`; throw new Error(message); } if (response.status === 204) return null; return await response.json(); } catch (error) { console.error(`[api.fetchApi][Coherence:Failed] Error fetching from ${endpoint}:`, error); addToast(error.message, 'error'); throw error; } } // [/DEF:fetchApi:Function] // [DEF:postApi:Function] // @PURPOSE: Generic POST request wrapper. // @PRE: endpoint and body are provided. // @POST: Returns Promise resolving to JSON data or throws on error. // @PARAM: endpoint (string) - API endpoint. // @PARAM: body (object) - Request payload. // @RETURN: Promise - JSON response. async function postApi(endpoint, body) { try { console.log(`[api.postApi][Action] Posting to context={{'endpoint': '${endpoint}'}}`); const response = await fetch(`${API_BASE_URL}${endpoint}`, { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify(body), }); console.log(`[api.postApi][Action] Received response context={{'status': ${response.status}, 'ok': ${response.ok}}}`); if (!response.ok) { const errorData = await response.json().catch(() => ({})); const message = errorData.detail ? (typeof errorData.detail === 'string' ? errorData.detail : JSON.stringify(errorData.detail)) : `API request failed with status ${response.status}`; throw new Error(message); } if (response.status === 204) return null; return await response.json(); } catch (error) { console.error(`[api.postApi][Coherence:Failed] Error posting to ${endpoint}:`, error); addToast(error.message, 'error'); throw error; } } // [/DEF:postApi:Function] // [DEF:requestApi:Function] // @PURPOSE: Generic request wrapper. // @PRE: endpoint and method are provided. // @POST: Returns Promise resolving to JSON data or throws on error. async function requestApi(endpoint, method = 'GET', body = null) { try { console.log(`[api.requestApi][Action] ${method} to context={{'endpoint': '${endpoint}'}}`); const options = { method, headers: getAuthHeaders(), }; if (body) { options.body = JSON.stringify(body); } const response = await fetch(`${API_BASE_URL}${endpoint}`, options); console.log(`[api.requestApi][Action] Received response context={{'status': ${response.status}, 'ok': ${response.ok}}}`); if (!response.ok) { const errorData = await response.json().catch(() => ({})); const message = errorData.detail ? (typeof errorData.detail === 'string' ? errorData.detail : JSON.stringify(errorData.detail)) : `API request failed with status ${response.status}`; console.error(`[api.requestApi][Action] Request failed context={{'status': ${response.status}, 'message': '${message}'}}`); throw new Error(message); } if (response.status === 204) { console.log('[api.requestApi][Action] 204 No Content received'); return null; } return await response.json(); } catch (error) { console.error(`[api.requestApi][Coherence:Failed] Error ${method} to ${endpoint}:`, error); addToast(error.message, 'error'); throw error; } } // [/DEF:requestApi:Function] // [DEF:api:Data] // @PURPOSE: API client object with specific methods. export const api = { fetchApi, postApi, requestApi, getPlugins: () => fetchApi('/plugins'), getTasks: (options = {}) => { 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((pluginId) => params.append('plugin_id', pluginId)); } const query = params.toString(); return fetchApi(`/tasks${query ? `?${query}` : ''}`); }, getTask: (taskId) => fetchApi(`/tasks/${taskId}`), createTask: (pluginId, params) => postApi('/tasks', { plugin_id: pluginId, params }), // Settings getSettings: () => fetchApi('/settings'), updateGlobalSettings: (settings) => requestApi('/settings/global', 'PATCH', settings), getEnvironments: () => fetchApi('/settings/environments'), addEnvironment: (env) => postApi('/settings/environments', env), updateEnvironment: (id, env) => requestApi(`/settings/environments/${id}`, 'PUT', env), deleteEnvironment: (id) => requestApi(`/settings/environments/${id}`, 'DELETE'), testEnvironmentConnection: (id) => postApi(`/settings/environments/${id}/test`, {}), updateEnvironmentSchedule: (id, schedule) => requestApi(`/environments/${id}/schedule`, 'PUT', schedule), getStorageSettings: () => fetchApi('/settings/storage'), updateStorageSettings: (storage) => requestApi('/settings/storage', 'PUT', storage), getEnvironmentsList: () => fetchApi('/environments'), getEnvironmentDatabases: (id) => fetchApi(`/environments/${id}/databases`), // Dashboards getDashboards: (envId, options = {}) => { 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); return fetchApi(`/dashboards?${params.toString()}`); }, getDatabaseMappings: (sourceEnvId, targetEnvId) => fetchApi(`/dashboards/db-mappings?source_env_id=${sourceEnvId}&target_env_id=${targetEnvId}`), // Datasets getDatasets: (envId, options = {}) => { 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); return fetchApi(`/datasets?${params.toString()}`); }, getDatasetIds: (envId, options = {}) => { const params = new URLSearchParams({ env_id: envId }); if (options.search) params.append('search', options.search); return fetchApi(`/datasets/ids?${params.toString()}`); }, getDatasetDetail: (envId, datasetId) => fetchApi(`/datasets/${datasetId}?env_id=${envId}`), // Settings getConsolidatedSettings: () => fetchApi('/settings/consolidated'), updateConsolidatedSettings: (settings) => requestApi('/settings/consolidated', 'PATCH', settings), }; // [/DEF:api:Data] // [/DEF:api_module:Module] // Export individual functions for easier use in components export { requestApi }; export const getPlugins = api.getPlugins; export const getTasks = api.getTasks; export const getTask = api.getTask; export const createTask = api.createTask; 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 updateConsolidatedSettings = api.updateConsolidatedSettings;