function normalizeApiBase(value) { if (value === undefined || value === null) return null const text = String(value).trim() if (!text || text === '.' || text === './') return '' return text.endsWith('/') ? text.slice(0, -1) : text } const API_BASE = normalizeApiBase(import.meta.env.VITE_API_BASE) ?? (import.meta.env.DEV ? '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 } export function apiUrl(path) { return `${API_BASE}${String(path || '')}` } 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 : Array.isArray(detail) ? detail .map((item) => { const loc = Array.isArray(item?.loc) ? item.loc.filter((part) => part !== 'body').join(' > ') : '' const msg = String(item?.msg || '').trim() if (loc && msg) return `${loc}: ${msg}` return msg || loc || '' }) .filter(Boolean) .join(' | ') : String(detail?.message || detail?.detail || 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 }), pesquisarLogradourosEixos: (limite = 20000) => getJson(`/api/pesquisa/logradouros-eixos?limite=${encodeURIComponent(String(limite))}`), 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') }, pesquisarLocalizacaoAvaliando(payload = {}) { return postJson('/api/pesquisa/localizar-avaliando', payload) }, pesquisarMapaModelos( modelosIds = [], avaliando = null, modoExibicao = 'pontos', criterioEspacial = 'maior_distancia', trabalhosTecnicosModelosModo = 'selecionados', trabalhosTecnicosProximidadeModo = 'sem_proximidade', trabalhosTecnicosRaioM = 1000, agruparPontosMercado = false, ) { const avaliandos = Array.isArray(avaliando) ? avaliando : [] const avaliandoUnico = !avaliandos.length && avaliando && typeof avaliando === 'object' ? avaliando : null return postJson('/api/pesquisa/mapa-modelos', { modelos_ids: modelosIds, avaliando_lat: avaliandoUnico?.lat ?? null, avaliando_lon: avaliandoUnico?.lon ?? null, avaliandos: avaliandos.length ? avaliandos : null, modo_exibicao: modoExibicao, criterio_espacial: criterioEspacial, trabalhos_tecnicos_modelos_modo: trabalhosTecnicosModelosModo, trabalhos_tecnicos_proximidade_modo: trabalhosTecnicosProximidadeModo, trabalhos_tecnicos_raio_m: trabalhosTecnicosRaioM, agrupar_pontos_mercado: Boolean(agruparPontosMercado), }) }, 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 }), pasteElaboracaoTable: (sessionId, pastedText) => postJson('/api/elaboracao/paste-table', { session_id: sessionId, pasted_text: pastedText }), confirmSheet: (sessionId, sheetName) => postJson('/api/elaboracao/confirm-sheet', { session_id: sessionId, sheet_name: sheetName }), applyImportColumns: (sessionId, colunas) => postJson('/api/elaboracao/apply-import-columns', { session_id: sessionId, colunas }), 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, options = {}) => postJson('/api/elaboracao/dispersao-interativo', { session_id: sessionId, alvo, ...(options || {}), }), 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 = null, indiceAvaliacao = null) => postJson('/api/elaboracao/evaluation/knn-details', { session_id: sessionId, valores_x: valoresX, indice_avaliacao: indiceAvaliacao, }), 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, observacaoModelo = '') => { 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, observacao_modelo: observacaoModelo, }), }) await handleResponse(response, path) return responseBlobWithDiagnostics(response, path) }, saveModelRepository: (sessionId, nomeArquivo, elaborador, observacaoModelo = '', confirmarSubstituicao = false) => postJson('/api/elaboracao/save-model-repository', { session_id: sessionId, nome_arquivo: nomeArquivo, elaborador, observacao_modelo: observacaoModelo, confirmar_substituicao: confirmarSubstituicao, }), exportBase: (sessionId, filtered = true) => getBlob(`/api/elaboracao/export-base?session_id=${encodeURIComponent(sessionId)}&filtered=${String(filtered)}`), updateElaboracaoMap: (sessionId, variavelMapa, modoMapa = 'pontos', pontoRaioMin = null, pontoRaioMax = null) => postJson('/api/elaboracao/map/update', { session_id: sessionId, variavel_mapa: variavelMapa, modo_mapa: modoMapa, ponto_raio_min: pontoRaioMin, ponto_raio_max: pontoRaioMax, }), updateElaboracaoResiduosMap: (sessionId, variavelMapa, modoMapa = 'pontos', escalaExtremoAbs = null, pontoRaioMin = null, pontoRaioMax = null) => postJson('/api/elaboracao/residuos/map/update', { session_id: sessionId, variavel_mapa: variavelMapa, modo_mapa: modoMapa, escala_extremo_abs: escalaExtremoAbs, ponto_raio_min: pontoRaioMin, ponto_raio_max: pontoRaioMax, }), 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 }), visualizacaoSection: (sessionId, secao, trabalhosTecnicosModelosModo = 'selecionados_e_outras_versoes') => postJson('/api/visualizacao/section', { session_id: sessionId, secao, trabalhos_tecnicos_modelos_modo: trabalhosTecnicosModelosModo, }), evaluationContextViz: (sessionId) => postJson('/api/visualizacao/evaluation/context', { session_id: sessionId }), updateVisualizacaoMap: (sessionId, variavelMapa, trabalhosTecnicosModelosModo = 'selecionados_e_outras_versoes', pontoRaioMin = null, pontoRaioMax = null) => postJson('/api/visualizacao/map/update', { session_id: sessionId, variavel_mapa: variavelMapa, trabalhos_tecnicos_modelos_modo: trabalhosTecnicosModelosModo, ponto_raio_min: pontoRaioMin, ponto_raio_max: pontoRaioMax, }), evaluationCalculateViz: (sessionId, valoresX, indiceBase, avaliando = null) => postJson('/api/visualizacao/evaluation/calculate', { session_id: sessionId, valores_x: valoresX, indice_base: indiceBase, avaliando_lat: avaliando?.lat ?? null, avaliando_lon: avaliando?.lon ?? null, }), evaluationKnnDetailsViz: (sessionId, valoresX, avaliando = null) => postJson('/api/visualizacao/evaluation/knn-details', { session_id: sessionId, valores_x: valoresX, avaliando_lat: avaliando?.lat ?? null, avaliando_lon: avaliando?.lon ?? null, }), evaluationLocationMapViz: (sessionId, avaliando = null) => postJson('/api/visualizacao/evaluation/location-map', { session_id: sessionId, avaliando_lat: avaliando?.lat ?? null, avaliando_lon: avaliando?.lon ?? null, }), exportEquationViz: (sessionId, mode = 'excel') => getBlob(`/api/visualizacao/equation/export?session_id=${encodeURIComponent(sessionId)}&mode=${encodeURIComponent(String(mode || 'excel'))}`), anexosModelos: () => getJson('/api/anexos/modelos'), anexosModeloColunas: (modeloId) => getJson(`/api/anexos/modelos/${encodeURIComponent(String(modeloId || ''))}/columns`), anexosGerar: (modeloId, bancoDadosColunas, avaliacao = null) => postJson('/api/anexos/generate', { modelo_id: modeloId, banco_dados_colunas: bancoDadosColunas, avaliacao, }), anexosDownload: (filename) => getBlob(`/api/anexos/download/${encodeURIComponent(String(filename || ''))}`), 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 }), trabalhosTecnicosListar: ({ pagina = 1, tamanhoPagina = 15, texto = '', tipo = '', ano = '', incluirIds = false } = {}) => { const params = new URLSearchParams({ pagina: String(pagina), tamanho_pagina: String(tamanhoPagina) }) if (texto) params.set('texto', texto) if (tipo) params.set('tipo', tipo) if (ano) params.set('ano', ano) if (incluirIds) params.set('incluir_ids', 'true') return getJson(`/api/trabalhos-tecnicos?${params.toString()}`) }, trabalhosTecnicosMapa: (trabalhosIds = null, modoExibicao = 'clusterizado', avaliando = null) => postJson('/api/trabalhos-tecnicos/mapa', { trabalhos_ids: trabalhosIds, modo_exibicao: modoExibicao, avaliando_lat: avaliando?.lat ?? null, avaliando_lon: avaliando?.lon ?? null, }), trabalhosTecnicosDetalhe: (trabalhoId) => postJson('/api/trabalhos-tecnicos/detalhe', { trabalho_id: trabalhoId }), trabalhosTecnicosCadastrar: (payload = {}) => postJson('/api/trabalhos-tecnicos/cadastrar', payload), trabalhosTecnicosSalvar: (payload = {}) => postJson('/api/trabalhos-tecnicos/salvar', payload), trabalhosTecnicosExcluir: (trabalhoId, confirmacao) => postJson('/api/trabalhos-tecnicos/excluir', { trabalho_id: trabalhoId, confirmacao }), trabalhosTecnicosModelosMl: () => getJson('/api/trabalhos-tecnicos/modelos-ml'), trabalhosTecnicosCadastrarModeloMl: (nome) => postJson('/api/trabalhos-tecnicos/modelos-ml', { nome }), 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') }, }