const API_BASE = import.meta.env.VITE_API_BASE || 'http://localhost:8000' const AUTH_TOKEN_STORAGE_KEY = 'mesa_auth_token' let AUTH_TOKEN = '' if (typeof window !== 'undefined') { AUTH_TOKEN = window.localStorage.getItem(AUTH_TOKEN_STORAGE_KEY) || '' } export function setAuthToken(token) { const value = String(token || '').trim() AUTH_TOKEN = value if (typeof window === 'undefined') return if (value) { window.localStorage.setItem(AUTH_TOKEN_STORAGE_KEY, value) } else { window.localStorage.removeItem(AUTH_TOKEN_STORAGE_KEY) } } export function getAuthToken() { return AUTH_TOKEN } function authHeaders(extraHeaders = {}) { const headers = { ...extraHeaders } if (AUTH_TOKEN) headers['X-Auth-Token'] = AUTH_TOKEN return headers } function emitAuthRequired(detail = 'Login obrigatorio') { if (typeof window === 'undefined') return const event = new CustomEvent('mesa:auth-required', { detail: { message: detail } }) window.dispatchEvent(event) } function describeFetchFailure(cause) { const name = String(cause?.name || '').trim() const message = String(cause?.message || '').trim() if (name === 'AbortError') { return 'a requisicao foi interrompida antes de concluir' } if (typeof navigator !== 'undefined' && navigator.onLine === false) { return 'sem conexao com a internet' } if (/failed to fetch|networkerror|load failed/i.test(message)) { return 'conexao interrompida durante a transferencia ou bloqueio de rede/CORS/proxy' } return message || 'falha de rede nao identificada' } function buildFetchError(path, stage, cause, status = null) { const endpoint = String(path || '').trim() || '(endpoint desconhecido)' const reason = describeFetchFailure(cause) const statusNumber = Number(status) const statusLabel = Number.isFinite(statusNumber) ? ` (HTTP ${statusNumber})` : '' const action = stage === 'body' ? `Falha ao receber dados de ${endpoint}${statusLabel}` : `Falha ao conectar com ${endpoint}` const error = new Error(`${action}. Motivo: ${reason}.`) error.path = endpoint error.stage = stage error.reason = reason if (Number.isFinite(statusNumber)) { error.status = statusNumber } error.cause = cause return error } async function fetchWithDiagnostics(path, options = {}) { try { return await fetch(`${API_BASE}${path}`, options) } catch (cause) { throw buildFetchError(path, 'request', cause) } } async function responseBlobWithDiagnostics(response, path = '') { try { return await response.blob() } catch (cause) { throw buildFetchError(path, 'body', cause, response?.status) } } async function handleResponse(response, path = '') { if (!response.ok) { let detail = 'Erro inesperado' try { const data = await response.json() detail = data.detail || detail } catch { detail = response.statusText || detail } const detailMessage = typeof detail === 'string' ? detail : String(detail?.message || response.statusText || 'Erro inesperado') if (response.status === 401 && path !== '/api/auth/login') { setAuthToken('') emitAuthRequired(detailMessage) } const error = new Error(detailMessage) error.status = response.status error.path = path error.detail = detail throw error } return response } async function postJson(path, body) { const response = await fetchWithDiagnostics(path, { method: 'POST', headers: authHeaders({ 'Content-Type': 'application/json' }), body: JSON.stringify(body), }) await handleResponse(response, path) return response.json() } async function postForm(path, formData) { const response = await fetchWithDiagnostics(path, { method: 'POST', headers: authHeaders(), body: formData, }) await handleResponse(response, path) return response.json() } async function getJson(path) { const response = await fetchWithDiagnostics(path, { headers: authHeaders() }) await handleResponse(response, path) return response.json() } async function getBlob(path) { const response = await fetchWithDiagnostics(path, { headers: authHeaders() }) await handleResponse(response, path) return responseBlobWithDiagnostics(response, path) } export function downloadBlob(blob, fileName) { const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = fileName document.body.appendChild(a) a.click() a.remove() URL.revokeObjectURL(url) } export const api = { authLogin: (usuario, matricula) => postJson('/api/auth/login', { usuario, matricula }), authMe: () => getJson('/api/auth/me'), authLogout: () => postJson('/api/auth/logout', {}), createSession: () => postJson('/api/sessions', {}), pesquisaAdminConfig: () => getJson('/api/pesquisa/admin-config'), pesquisaAdminConfigSalvar: (campos = {}) => postJson('/api/pesquisa/admin-config', { campos }), pesquisarModelos(filtros = {}) { const params = new URLSearchParams() Object.entries(filtros).forEach(([key, value]) => { if (value === null || value === undefined) return const text = String(value).trim() if (!text) return params.append(key, text) }) const query = params.toString() return getJson(query ? `/api/pesquisa/modelos?${query}` : '/api/pesquisa/modelos') }, pesquisarMapaModelos(modelosIds = []) { return postJson('/api/pesquisa/mapa-modelos', { modelos_ids: modelosIds }) }, uploadElaboracaoFile(sessionId, file) { const form = new FormData() form.append('session_id', sessionId) form.append('file', file) return postForm('/api/elaboracao/upload', form) }, elaboracaoRepositorioModelos: () => getJson('/api/elaboracao/repositorio-modelos'), elaboracaoRepositorioCarregar: (sessionId, modeloId) => postJson('/api/elaboracao/repositorio-carregar', { session_id: sessionId, modelo_id: modeloId }), confirmSheet: (sessionId, sheetName) => postJson('/api/elaboracao/confirm-sheet', { session_id: sessionId, sheet_name: sheetName }), mapCoords: (sessionId, colLat, colLon) => postJson('/api/elaboracao/map-coords', { session_id: sessionId, col_lat: colLat, col_lon: colLon }), geocodificar: (sessionId, colCdlog, colNum, auto200) => postJson('/api/elaboracao/geocodificar', { session_id: sessionId, col_cdlog: colCdlog, col_num: colNum, auto_200: auto200 }), geocodificarCorrecoes: (sessionId, correcoes, auto200) => postJson('/api/elaboracao/geocodificar-correcoes', { session_id: sessionId, correcoes, auto_200: auto200 }), geocodificarReiniciar: (sessionId) => postJson('/api/elaboracao/geocodificar-reiniciar', { session_id: sessionId }), geocodificarExcluirCoords: (sessionId) => postJson('/api/elaboracao/geocodificar-excluir-coords', { session_id: sessionId }), applySelection(payload) { return postJson('/api/elaboracao/apply-selection', payload) }, classifyElaboracaoX: (sessionId, colunasX) => postJson('/api/elaboracao/classify-x', { session_id: sessionId, colunas_x: colunasX }), searchTransformations: (sessionId, grauCoef, grauF, transformacoesFixas = {}, transformacaoYFixa = 'Livre') => postJson('/api/elaboracao/search-transformations', { session_id: sessionId, grau_min_coef: grauCoef, grau_min_f: grauF, transformacoes_fixas: transformacoesFixas, transformacao_y_fixa: transformacaoYFixa, }), adoptSuggestion: (sessionId, indice) => postJson('/api/elaboracao/adopt-suggestion', { session_id: sessionId, indice }), fitModel(payload) { return postJson('/api/elaboracao/fit-model', payload) }, updateModelDispersao: (sessionId, payload) => postJson('/api/elaboracao/model-dispersao', { session_id: sessionId, ...(payload || {}), }), getDispersaoInterativo: (sessionId, alvo) => postJson('/api/elaboracao/dispersao-interativo', { session_id: sessionId, alvo, }), getDiagnosticoInterativo: (sessionId, grafico) => postJson('/api/elaboracao/diagnostico-interativo', { session_id: sessionId, grafico, }), previewTransformElab: (sessionId, transformacaoY, transformacoesX) => postJson('/api/elaboracao/transform-preview', { session_id: sessionId, transformacao_y: transformacaoY, transformacoes_x: transformacoesX, }), applyOutlierFilters: (sessionId, filtros) => postJson('/api/elaboracao/outliers/apply-filters', { session_id: sessionId, filtros }), applyOutlierFiltersRecursive: (sessionId, filtros) => postJson('/api/elaboracao/outliers/apply-filters-recursive', { session_id: sessionId, filtros }), restartOutlierIteration: (sessionId, outliersTexto, reincluirTexto, grauCoef, grauF) => postJson('/api/elaboracao/outliers/restart', { session_id: sessionId, outliers_texto: outliersTexto, reincluir_texto: reincluirTexto, grau_min_coef: grauCoef, grau_min_f: grauF, }), outlierSummary: (sessionId, outliersTexto, reincluirTexto) => postJson('/api/elaboracao/outliers/summary', { session_id: sessionId, outliers_texto: outliersTexto, reincluir_texto: reincluirTexto, }), clearOutlierHistory: (sessionId) => postJson('/api/elaboracao/outliers/clear-history', { session_id: sessionId }), evaluationFieldsElab: (sessionId) => postJson('/api/elaboracao/evaluation/fields', { session_id: sessionId }), evaluationCalculateElab: (sessionId, valoresX, indiceBase) => postJson('/api/elaboracao/evaluation/calculate', { session_id: sessionId, valores_x: valoresX, indice_base: indiceBase, }), evaluationKnnDetailsElab: (sessionId, valoresX) => postJson('/api/elaboracao/evaluation/knn-details', { session_id: sessionId, valores_x: valoresX, }), evaluationClearElab: (sessionId) => postJson('/api/elaboracao/evaluation/clear', { session_id: sessionId }), evaluationDeleteElab: (sessionId, indice, indiceBase) => postJson('/api/elaboracao/evaluation/delete', { session_id: sessionId, indice, indice_base: indiceBase, }), getAvaliadores: () => getJson('/api/elaboracao/avaliadores'), evaluationBaseElab: (sessionId, indiceBase) => postJson('/api/elaboracao/evaluation/base', { session_id: sessionId, indice_base: indiceBase, }), exportEvaluationElab: async (sessionId) => getBlob(`/api/elaboracao/evaluation/export?session_id=${encodeURIComponent(sessionId)}`), exportEquationElab: async (sessionId, mode = 'excel') => getBlob(`/api/elaboracao/equation/export?session_id=${encodeURIComponent(sessionId)}&mode=${encodeURIComponent(String(mode || 'excel'))}`), exportModel: async (sessionId, nomeArquivo, elaborador) => { const path = '/api/elaboracao/export-model' const response = await fetchWithDiagnostics(path, { method: 'POST', headers: authHeaders({ 'Content-Type': 'application/json' }), body: JSON.stringify({ session_id: sessionId, nome_arquivo: nomeArquivo, elaborador }), }) await handleResponse(response, path) return responseBlobWithDiagnostics(response, path) }, exportBase: (sessionId, filtered = true) => getBlob(`/api/elaboracao/export-base?session_id=${encodeURIComponent(sessionId)}&filtered=${String(filtered)}`), updateElaboracaoMap: (sessionId, variavelMapa, modoMapa = 'pontos') => postJson('/api/elaboracao/map/update', { session_id: sessionId, variavel_mapa: variavelMapa, modo_mapa: modoMapa, }), updateElaboracaoResiduosMap: (sessionId, variavelMapa, modoMapa = 'pontos', escalaExtremoAbs = null) => postJson('/api/elaboracao/residuos/map/update', { session_id: sessionId, variavel_mapa: variavelMapa, modo_mapa: modoMapa, escala_extremo_abs: escalaExtremoAbs, }), previewMarketDateColumn: (sessionId, colunaData) => postJson('/api/elaboracao/market-date/preview', { session_id: sessionId, coluna_data: colunaData }), applyMarketDateColumn: (sessionId, colunaData) => postJson('/api/elaboracao/market-date/apply', { session_id: sessionId, coluna_data: colunaData }), getContext: (sessionId) => getJson(`/api/elaboracao/context?session_id=${encodeURIComponent(sessionId)}`), uploadVisualizacaoFile(sessionId, file) { const form = new FormData() form.append('session_id', sessionId) form.append('file', file) return postForm('/api/visualizacao/upload', form) }, visualizacaoRepositorioModelos: () => getJson('/api/visualizacao/repositorio-modelos'), visualizacaoRepositorioCarregar: (sessionId, modeloId) => postJson('/api/visualizacao/repositorio-carregar', { session_id: sessionId, modelo_id: modeloId }), exibirVisualizacao: (sessionId) => postJson('/api/visualizacao/exibir', { session_id: sessionId }), evaluationContextViz: (sessionId) => postJson('/api/visualizacao/evaluation/context', { session_id: sessionId }), updateVisualizacaoMap: (sessionId, variavelMapa) => postJson('/api/visualizacao/map/update', { session_id: sessionId, variavel_mapa: variavelMapa }), evaluationFieldsViz: (sessionId) => postJson('/api/visualizacao/evaluation/fields', { session_id: sessionId }), evaluationCalculateViz: (sessionId, valoresX, indiceBase) => postJson('/api/visualizacao/evaluation/calculate', { session_id: sessionId, valores_x: valoresX, indice_base: indiceBase, }), evaluationKnnDetailsViz: (sessionId, valoresX) => postJson('/api/visualizacao/evaluation/knn-details', { session_id: sessionId, valores_x: valoresX, }), evaluationClearViz: (sessionId) => postJson('/api/visualizacao/evaluation/clear', { session_id: sessionId }), evaluationDeleteViz: (sessionId, indice, indiceBase) => postJson('/api/visualizacao/evaluation/delete', { session_id: sessionId, indice, indice_base: indiceBase, }), evaluationBaseViz: (sessionId, indiceBase) => postJson('/api/visualizacao/evaluation/base', { session_id: sessionId, indice_base: indiceBase, }), exportEvaluationViz: (sessionId) => getBlob(`/api/visualizacao/evaluation/export?session_id=${encodeURIComponent(sessionId)}`), exportEquationViz: (sessionId, mode = 'excel') => getBlob(`/api/visualizacao/equation/export?session_id=${encodeURIComponent(sessionId)}&mode=${encodeURIComponent(String(mode || 'excel'))}`), clearVisualizacao: (sessionId) => postJson('/api/visualizacao/clear', { session_id: sessionId }), repositorioListar: () => getJson('/api/repositorio/modelos'), repositorioUpload(files = [], { confirmarSubstituicao = false } = {}) { const form = new FormData() files.forEach((file) => { form.append('files', file) }) form.append('confirmar_substituicao', confirmarSubstituicao ? 'true' : 'false') return postForm('/api/repositorio/upload', form) }, repositorioDelete: (modelosIds = []) => postJson('/api/repositorio/delete', { modelos_ids: modelosIds }), logsStatus: () => getJson('/api/logs/status'), logsEvents({ scope = '', usuario = '', limit = 200 } = {}) { const params = new URLSearchParams() const scopeText = String(scope || '').trim() const usuarioText = String(usuario || '').trim() const limitNumber = Number(limit) if (scopeText) params.set('scope', scopeText) if (usuarioText) params.set('usuario', usuarioText) if (Number.isFinite(limitNumber) && limitNumber > 0) params.set('limit', String(Math.floor(limitNumber))) const query = params.toString() return getJson(query ? `/api/logs/events?${query}` : '/api/logs/events') }, }