From f29e61f3350c5a341038975068de1a2a3fa0e2ec Mon Sep 17 00:00:00 2001 From: root Date: Tue, 17 Mar 2026 11:28:43 -0300 Subject: [PATCH 01/16] =?UTF-8?q?chore:=20sync=20prod=20state=20=E2=80=94?= =?UTF-8?q?=20LiteLLM/LLMFit=20integration=20+=20compose=20updates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - docker-compose.prod.yml: LiteLLM na rede coolify, LLMFIT_BASE_URL/API_KEY, rede coolify external - docker/litellm-config.yaml: TIER 1 LLMFit ativo, fallback Ollama configurado - Dockerfile: ajustes de build - server/: novos módulos SOE, plus-sync, schemas compartilhados --- Dockerfile | 9 ++-- client/src/contexts/SoeMotorContext.tsx | 17 +++++++ client/src/pages/SOE.tsx | 7 +++ docker-compose.prod.yml | 18 +++++-- docker/litellm-config.yaml | 66 +++++++++++-------------- server/bi/upload.ts | 6 +-- server/erp/routes.ts | 3 ++ server/index.ts | 2 +- server/modules/loader.ts | 6 +++ server/retail/plus-sync.ts | 9 ++++ shared/schemas/index.ts | 3 ++ 11 files changed, 96 insertions(+), 50 deletions(-) create mode 100644 client/src/contexts/SoeMotorContext.tsx create mode 100644 client/src/pages/SOE.tsx create mode 100644 server/modules/loader.ts create mode 100644 server/retail/plus-sync.ts create mode 100644 shared/schemas/index.ts diff --git a/Dockerfile b/Dockerfile index ddb0688..ddf659c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ FROM node:20-alpine AS builder WORKDIR /app COPY package*.json ./ -RUN npm ci --ignore-scripts +RUN npm ci COPY . . RUN npm run build @@ -18,16 +18,19 @@ ENV NODE_ENV=production # Deps de produção apenas COPY package*.json ./ -RUN npm ci --omit=dev --ignore-scripts +RUN npm ci --omit=dev # Artefatos do build COPY --from=builder /app/dist ./dist COPY --from=builder /app/shared ./shared COPY --from=builder /app/migrations ./migrations +# Arquivos de runtime necessários (caminhos quebrados pelo bundler) +RUN cp /app/node_modules/connect-pg-simple/table.sql /app/dist/table.sql + EXPOSE 5000 HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ - CMD wget -qO- http://localhost:5000/api/health || exit 1 + CMD wget -qO- http://127.0.0.1:5000/ || exit 1 CMD ["node", "dist/index.cjs"] diff --git a/client/src/contexts/SoeMotorContext.tsx b/client/src/contexts/SoeMotorContext.tsx new file mode 100644 index 0000000..996a6ce --- /dev/null +++ b/client/src/contexts/SoeMotorContext.tsx @@ -0,0 +1,17 @@ +import { createContext, useContext, ReactNode } from "react"; + +interface SoeMotorContextType {} + +const SoeMotorContext = createContext({}); + +export function SoeMotorProvider({ children }: { children: ReactNode }) { + return ( + + {children} + + ); +} + +export function useSoeMotor() { + return useContext(SoeMotorContext); +} diff --git a/client/src/pages/SOE.tsx b/client/src/pages/SOE.tsx new file mode 100644 index 0000000..ca173a7 --- /dev/null +++ b/client/src/pages/SOE.tsx @@ -0,0 +1,7 @@ +export default function SOE() { + return ( +
+

SOE — em desenvolvimento

+
+ ); +} diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index af01a8c..ddb2cd6 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -72,12 +72,15 @@ services: condition: service_started networks: - arcadia-internal - - arcadia-public + - coolify labels: - "traefik.enable=true" + - "traefik.docker.network=coolify" + - "traefik.http.routers.arcadia.entrypoints=https" - "traefik.http.routers.arcadia.rule=Host(`${DOMAIN}`)" - "traefik.http.routers.arcadia.tls=true" - "traefik.http.routers.arcadia.tls.certresolver=letsencrypt" + - "traefik.http.services.arcadia.loadbalancer.server.port=5000" # ── Microserviços Python ───────────────────────────────────────────────────── contabil: @@ -153,6 +156,9 @@ services: SERVICE_NAME: embeddings SERVICE_PORT: 8001 DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/${PGDATABASE:-arcadia} + LITELLM_BASE_URL: http://litellm:4000 + LITELLM_API_KEY: ${LITELLM_API_KEY} + OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:-http://ollama:11434} depends_on: db: condition: service_healthy @@ -170,12 +176,13 @@ services: environment: OPENAI_API_KEY: ${OPENAI_API_KEY:-} LITELLM_MASTER_KEY: ${LITELLM_API_KEY} - DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/${PGDATABASE:-arcadia} + DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/litellm # Ollama: se instalado no host use http://host-gateway:11434 # Se usar container Docker, mantém http://ollama:11434 OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:-http://ollama:11434} # LLMFit: URL do serviço de modelos fine-tuned LLMFIT_BASE_URL: ${LLMFIT_BASE_URL:-} + LLMFIT_API_KEY: ${LLMFIT_API_KEY:-} # Providers externos opcionais (soberania: só habilitados se configurados) ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} GROQ_API_KEY: ${GROQ_API_KEY:-} @@ -184,6 +191,7 @@ services: condition: service_healthy networks: - arcadia-internal + - coolify # ── Ollama (LLMs locais — soberania total) ──────────────────────────────────── # OPÇÃO A (padrão): Ollama como container Docker @@ -215,7 +223,7 @@ services: - litellm networks: - arcadia-internal - - arcadia-public + - coolify labels: - "traefik.enable=true" - "traefik.http.routers.webui.rule=Host(`ai.${DOMAIN}`)" @@ -227,8 +235,8 @@ services: networks: arcadia-internal: driver: bridge - arcadia-public: - driver: bridge + coolify: + external: true volumes: pgdata: diff --git a/docker/litellm-config.yaml b/docker/litellm-config.yaml index 336d872..0085857 100644 --- a/docker/litellm-config.yaml +++ b/docker/litellm-config.yaml @@ -13,35 +13,22 @@ model_list: # ── TIER 1: LLMFit (modelos fine-tuned locais — máxima soberania) ──────────── - # Descomente quando o LLMFit estiver rodando no servidor - # O LLMFit expõe API compatível com OpenAI — basta apontar a URL - # - # - model_name: arcadia-finetuned - # litellm_params: - # model: openai/arcadia-v1 # nome do modelo no LLMFit - # api_base: os.environ/LLMFIT_BASE_URL - # api_key: llmfit-internal - # - # - model_name: arcadia-embed - # litellm_params: - # model: openai/arcadia-embed-v1 # modelo de embeddings fine-tuned - # api_base: os.environ/LLMFIT_BASE_URL - # api_key: llmfit-internal + - model_name: arcadia-finetuned + litellm_params: + model: openai/llama3.2:3b + api_base: os.environ/LLMFIT_BASE_URL + api_key: os.environ/LLMFIT_API_KEY + + - model_name: arcadia-embed + litellm_params: + model: openai/nomic-embed-text:latest + api_base: os.environ/LLMFIT_BASE_URL + api_key: os.environ/LLMFIT_API_KEY # ── TIER 2: Ollama (LLMs locais — soberania total) ─────────────────────────── - - model_name: llama3.3 + - model_name: llama3.2 litellm_params: - model: ollama/llama3.3 - api_base: os.environ/OLLAMA_BASE_URL - - - model_name: qwen2.5-coder - litellm_params: - model: ollama/qwen2.5-coder:7b - api_base: os.environ/OLLAMA_BASE_URL - - - model_name: deepseek-r1 - litellm_params: - model: ollama/deepseek-r1:7b + model: ollama/llama3.2:3b api_base: os.environ/OLLAMA_BASE_URL - model_name: nomic-embed-text @@ -49,6 +36,12 @@ model_list: model: ollama/nomic-embed-text api_base: os.environ/OLLAMA_BASE_URL + # Alias OpenAI-compatible para embedding (Manus usa este nome por padrão) + - model_name: text-embedding-3-small + litellm_params: + model: ollama/nomic-embed-text + api_base: os.environ/OLLAMA_BASE_URL + # ── TIER 3: OpenAI (opt-in — só ativo se OPENAI_API_KEY configurado) ───────── - model_name: gpt-4o litellm_params: @@ -73,29 +66,26 @@ model_list: # api_key: os.environ/GROQ_API_KEY # ── Modelo padrão do Arcádia (Manus usa este) ───────────────────────────────── - # Prioridade: LLMFit → OpenAI (se configurado) → Ollama (sempre disponível) - # Para soberania total: remova o fallback para gpt-4o-mini + # Prioridade: LLMFit (TIER 1) → Ollama (TIER 2 — fallback) - model_name: arcadia-default litellm_params: - model: ollama/llama3.3 - api_base: os.environ/OLLAMA_BASE_URL - model_info: - # fallbacks: ["gpt-4o-mini"] # descomente para habilitar fallback externo + model: openai/llama3.2:3b + api_base: os.environ/LLMFIT_BASE_URL + api_key: os.environ/LLMFIT_API_KEY router_settings: routing_strategy: least-busy fallbacks: - - {"gpt-4o": ["llama3.3"]} - - {"gpt-4o-mini": ["llama3.3"]} - - {"arcadia-default": ["llama3.3"]} + - {"gpt-4o": ["llama3.2"]} + - {"gpt-4o-mini": ["llama3.2"]} + - {"arcadia-default": ["llama3.2"]} + - {"arcadia-finetuned": ["llama3.2"]} + - {"arcadia-embed": ["nomic-embed-text"]} litellm_settings: drop_params: true request_timeout: 120 set_verbose: false - # Loga todas as chamadas no banco — essencial para auditoria e soberania - success_callback: ["langfuse"] - failure_callback: ["langfuse"] general_settings: master_key: os.environ/LITELLM_MASTER_KEY diff --git a/server/bi/upload.ts b/server/bi/upload.ts index 5ebe16f..f270556 100644 --- a/server/bi/upload.ts +++ b/server/bi/upload.ts @@ -2,15 +2,15 @@ import type { Express, Request, Response } from "express"; import multer from "multer"; import path from "path"; import fs from "fs"; -import { createRequire } from "module"; + import AdmZip from "adm-zip"; import * as BSON from "bson"; import { db } from "../../db/index"; import { biDatasets, stagedTables } from "@shared/schema"; import { sql } from "drizzle-orm"; -const require = createRequire(import.meta.url); -const XLSX = require("xlsx"); + +import * as XLSX from "xlsx"; const uploadDir = path.join(process.cwd(), "uploads"); if (!fs.existsSync(uploadDir)) { diff --git a/server/erp/routes.ts b/server/erp/routes.ts index f8d36ba..1280db7 100644 --- a/server/erp/routes.ts +++ b/server/erp/routes.ts @@ -1136,3 +1136,6 @@ export function registerErpRoutes(app: Express): void { } }); } +// SOE routes — placeholder + +export function registerSoeRoutes(_app: Express): void {} diff --git a/server/index.ts b/server/index.ts index 20654ef..f980fb9 100644 --- a/server/index.ts +++ b/server/index.ts @@ -346,7 +346,7 @@ export function log(message: string, source = "express") { password: hashedPassword, name: "Administrador Master", email: "admin@arcadia.suite", - role: "master", + role: "admin", status: "active", }); console.log("[Seed] Usuário master 'admin' criado com sucesso"); diff --git a/server/modules/loader.ts b/server/modules/loader.ts new file mode 100644 index 0000000..2c8b310 --- /dev/null +++ b/server/modules/loader.ts @@ -0,0 +1,6 @@ +import type { Express } from "express"; + +// Carregador de rotas modulares — placeholder +export async function loadModuleRoutes(_app: Express): Promise { + // Módulos serão registrados aqui conforme forem criados +} diff --git a/server/retail/plus-sync.ts b/server/retail/plus-sync.ts new file mode 100644 index 0000000..b633698 --- /dev/null +++ b/server/retail/plus-sync.ts @@ -0,0 +1,9 @@ +// Serviço de sincronização com Plus — placeholder +export const retailPlusSyncService = { + async getPlusStatus() { return { connected: false }; }, + async syncAllPersonsToPlus(_tenantId: any, _empresaId: any) { return { synced: 0 }; }, + async syncSaleToPlus(_saleId: any) { return { synced: false }; }, + async emitirNFeSale(_saleId: any, _tipo?: string) { return { emitted: false }; }, + async importClientesFromPlus(_tenantId: any, _empresaId: any) { return { imported: 0 }; }, + async importProdutosFromPlus(_tenantId: any, _empresaId: any) { return { imported: 0 }; }, +}; diff --git a/shared/schemas/index.ts b/shared/schemas/index.ts new file mode 100644 index 0000000..41a9f22 --- /dev/null +++ b/shared/schemas/index.ts @@ -0,0 +1,3 @@ +// Schemas modulares — gerados pelo Dev Center +// Arquivo placeholder: adicione exports conforme módulos forem criados +export {}; From 64c413d131a2e21ed1bc4621ddcf3301fb8c361b Mon Sep 17 00:00:00 2001 From: root Date: Tue, 17 Mar 2026 11:33:24 -0300 Subject: [PATCH 02/16] =?UTF-8?q?feat:=20add=20Superset=20BI=20(profile=20?= =?UTF-8?q?bi)=20=E2=80=94=20bi.suite.onboardbi.com.br?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.prod.yml | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index ddb2cd6..2d8ec72 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -232,6 +232,46 @@ services: - "traefik.http.services.webui.loadbalancer.server.port=8080" profiles: [ai] + # ── Superset BI ─────────────────────────────────────────────────────────────── + superset: + image: apache/superset:latest + restart: always + environment: + SUPERSET_SECRET_KEY: ${SUPERSET_SECRET_KEY} + SQLALCHEMY_DATABASE_URI: postgresql+psycopg2://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/superset + SUPERSET_LOAD_EXAMPLES: "no" + SUPERSET_ADMIN_USERNAME: ${SUPERSET_ADMIN_USERNAME:-admin} + SUPERSET_ADMIN_PASSWORD: ${SUPERSET_ADMIN_PASSWORD} + SUPERSET_ADMIN_EMAIL: ${SUPERSET_ADMIN_EMAIL:-admin@onboardbi.com.br} + command: > + /bin/sh -c " + superset db upgrade && + superset fab create-admin + --username \"$$SUPERSET_ADMIN_USERNAME\" + --firstname Arcadia --lastname Admin + --email \"$$SUPERSET_ADMIN_EMAIL\" + --password \"$$SUPERSET_ADMIN_PASSWORD\" || true && + superset init && + gunicorn --bind 0.0.0.0:8088 --workers 2 --timeout 120 'superset.app:create_app()' + " + volumes: + - superset_home:/app/superset_home + depends_on: + db: + condition: service_healthy + networks: + - arcadia-internal + - coolify + labels: + - "traefik.enable=true" + - "traefik.docker.network=coolify" + - "traefik.http.routers.superset.entrypoints=https" + - "traefik.http.routers.superset.rule=Host(`bi.${DOMAIN}`)" + - "traefik.http.routers.superset.tls=true" + - "traefik.http.routers.superset.tls.certresolver=letsencrypt" + - "traefik.http.services.superset.loadbalancer.server.port=8088" + profiles: [bi] + networks: arcadia-internal: driver: bridge @@ -243,3 +283,4 @@ volumes: redis_data: ollama_models: open_webui_data: + superset_home: From 98b79be03d42e301a7371727b06fb0474c3b51fb Mon Sep 17 00:00:00 2001 From: root Date: Tue, 17 Mar 2026 12:05:39 -0300 Subject: [PATCH 03/16] =?UTF-8?q?feat:=20Superset=20BI=20funcional=20?= =?UTF-8?q?=E2=80=94=20psycopg2,=20EMBEDDED=5FSUPERSET,=20gunicorn?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.prod.yml | 23 +++++++++------------- docker/superset/Dockerfile | 5 +++++ docker/superset/init.sh | 23 ++++++++++++++++++++++ docker/superset/superset_config.py | 31 ++++++++++++++++++++++++++++++ 4 files changed, 68 insertions(+), 14 deletions(-) create mode 100644 docker/superset/Dockerfile create mode 100755 docker/superset/init.sh create mode 100644 docker/superset/superset_config.py diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 2d8ec72..ea67982 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -234,27 +234,22 @@ services: # ── Superset BI ─────────────────────────────────────────────────────────────── superset: - image: apache/superset:latest + build: + context: ./docker/superset + dockerfile: Dockerfile restart: always environment: SUPERSET_SECRET_KEY: ${SUPERSET_SECRET_KEY} - SQLALCHEMY_DATABASE_URI: postgresql+psycopg2://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/superset - SUPERSET_LOAD_EXAMPLES: "no" + SQLALCHEMY_DATABASE_URI: postgresql://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/superset + PYTHONPATH: /app/pythonpath SUPERSET_ADMIN_USERNAME: ${SUPERSET_ADMIN_USERNAME:-admin} SUPERSET_ADMIN_PASSWORD: ${SUPERSET_ADMIN_PASSWORD} SUPERSET_ADMIN_EMAIL: ${SUPERSET_ADMIN_EMAIL:-admin@onboardbi.com.br} - command: > - /bin/sh -c " - superset db upgrade && - superset fab create-admin - --username \"$$SUPERSET_ADMIN_USERNAME\" - --firstname Arcadia --lastname Admin - --email \"$$SUPERSET_ADMIN_EMAIL\" - --password \"$$SUPERSET_ADMIN_PASSWORD\" || true && - superset init && - gunicorn --bind 0.0.0.0:8088 --workers 2 --timeout 120 'superset.app:create_app()' - " + ARCADIA_DATABASE_URL: postgresql+psycopg2://${PGUSER:-arcadia}:${PGPASSWORD}@db:5432/${PGDATABASE:-arcadia} + command: ["/bin/bash", "/app/docker/init.sh"] volumes: + - ./docker/superset/superset_config.py:/app/pythonpath/superset_config.py:ro + - ./docker/superset/init.sh:/app/docker/init.sh:ro - superset_home:/app/superset_home depends_on: db: diff --git a/docker/superset/Dockerfile b/docker/superset/Dockerfile new file mode 100644 index 0000000..923ddcf --- /dev/null +++ b/docker/superset/Dockerfile @@ -0,0 +1,5 @@ +FROM apache/superset:latest +USER root +RUN pip install psycopg2-binary && \ + sed -i 's/include-system-site-packages = false/include-system-site-packages = true/' /app/.venv/pyvenv.cfg +USER superset diff --git a/docker/superset/init.sh b/docker/superset/init.sh new file mode 100755 index 0000000..1448a39 --- /dev/null +++ b/docker/superset/init.sh @@ -0,0 +1,23 @@ +#!/bin/bash +set -e + +echo "[Superset Init] Iniciando configuracao..." +superset db upgrade + +superset fab create-admin \ + --username "${SUPERSET_ADMIN_USERNAME:-admin}" \ + --firstname "Arcadia" \ + --lastname "Admin" \ + --email "${SUPERSET_ADMIN_EMAIL:-admin@onboardbi.com.br}" \ + --password "${SUPERSET_ADMIN_PASSWORD:-arcadia1337}" 2>/dev/null || true + +superset init + +echo "[Superset Init] Iniciando servidor..." +gunicorn \ + --bind "0.0.0.0:8088" \ + --access-logfile "-" \ + --error-logfile "-" \ + --workers 2 \ + --timeout 120 \ + "superset.app:create_app()" diff --git a/docker/superset/superset_config.py b/docker/superset/superset_config.py new file mode 100644 index 0000000..25233b5 --- /dev/null +++ b/docker/superset/superset_config.py @@ -0,0 +1,31 @@ +import os + +SECRET_KEY = os.environ.get("SUPERSET_SECRET_KEY", "change-in-production") + +SQLALCHEMY_DATABASE_URI = os.environ.get( + "SQLALCHEMY_DATABASE_URI", + "postgresql://arcadia:arcadia123@db:5432/superset" +) + +ENABLE_CORS = True +CORS_OPTIONS = { + "supports_credentials": True, + "allow_headers": ["*"], + "resources": {r"/api/*": {"origins": "*"}}, +} + +FEATURE_FLAGS = { + "EMBEDDED_SUPERSET": True, + "ENABLE_TEMPLATE_PROCESSING": True, + "ALERT_REPORTS": True, + "DRILL_TO_DETAIL": True, +} + +SUPERSET_WEBSERVER_TIMEOUT = 300 + +CACHE_CONFIG = { + "CACHE_TYPE": "SimpleCache", + "CACHE_DEFAULT_TIMEOUT": 300, +} + +APP_NAME = "Arcadia Insights" From 5571cb51e6cab9b2941098d219e81b481abb3470 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 17 Mar 2026 13:20:10 -0300 Subject: [PATCH 04/16] =?UTF-8?q?feat:=20Superset=20gateway=20=E2=80=94=20?= =?UTF-8?q?proxy=20/superset/*,=20guest-token,=20dashboards=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/routes.ts | 5 ++ server/superset/proxy.ts | 39 ++++++++++ server/superset/routes.ts | 151 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 195 insertions(+) create mode 100644 server/superset/proxy.ts create mode 100644 server/superset/routes.ts diff --git a/server/routes.ts b/server/routes.ts index 1b0fe2a..8b5fd26 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -48,6 +48,8 @@ import xosRoutes from "./xos/routes"; import governanceRoutes from "./governance/routes"; import { setupPlusProxy } from "./plus/proxy"; import { setupMetabaseProxy } from "./metabase/proxy"; +import { setupSupersetProxy } from "./superset/proxy"; +import { registerSupersetRoutes } from "./superset/routes"; import { registerEngineRoomRoutes } from "./engine-room/routes"; import { registerMetaSetRoutes } from "./metaset/routes"; import plusSsoRoutes from "./plus/sso"; @@ -70,6 +72,8 @@ export async function registerRoutes( // Arcádia Plus - Proxy registered AFTER session but BEFORE auth-protected routes await setupPlusProxy(app); + // Superset BI - Proxy + setupSupersetProxy(app); // Metabase BI - Proxy to Metabase instance setupMetabaseProxy(app); @@ -87,6 +91,7 @@ export async function registerRoutes( app.use("/api/graph", graphRoutes); registerBiEngineRoutes(app); registerMetaSetRoutes(app); + registerSupersetRoutes(app); registerCommEngineRoutes(app); registerLearningRoutes(app); app.use("/api/compass", compassRoutes); diff --git a/server/superset/proxy.ts b/server/superset/proxy.ts new file mode 100644 index 0000000..b17a1b5 --- /dev/null +++ b/server/superset/proxy.ts @@ -0,0 +1,39 @@ +import { Express, Response } from "express"; +import { createProxyMiddleware } from "http-proxy-middleware"; + +const SUPERSET_HOST = process.env.SUPERSET_HOST || "superset"; +const SUPERSET_PORT = parseInt(process.env.SUPERSET_PORT || "8088", 10); +const SUPERSET_TIMEOUT = 60000; + +export function setupSupersetProxy(app: Express): void { + const target = `http://${SUPERSET_HOST}:${SUPERSET_PORT}`; + + const supersetProxy = createProxyMiddleware({ + target, + changeOrigin: true, + timeout: SUPERSET_TIMEOUT, + proxyTimeout: SUPERSET_TIMEOUT, + pathRewrite: { "^/superset": "" }, + on: { + error: (err, _req, res) => { + console.error("[Superset Proxy] Error:", err.message); + if (res && typeof (res as Response).status === "function") { + (res as Response).status(502).json({ + error: "Superset indisponível", + message: "O Superset está iniciando. Tente novamente em alguns segundos.", + target, + }); + } + }, + proxyRes: (proxyRes) => { + const location = proxyRes.headers["location"]; + if (location && typeof location === "string" && location.startsWith("/")) { + proxyRes.headers["location"] = `/superset${location}`; + } + }, + }, + }); + + app.use("/superset", supersetProxy); + console.log(`[Superset Proxy] Configurado -> /superset/* => ${target}`); +} diff --git a/server/superset/routes.ts b/server/superset/routes.ts new file mode 100644 index 0000000..aee867a --- /dev/null +++ b/server/superset/routes.ts @@ -0,0 +1,151 @@ +import type { Express, Request, Response } from "express"; + +const SUPERSET_HOST = process.env.SUPERSET_HOST || "superset"; +const SUPERSET_PORT = parseInt(process.env.SUPERSET_PORT || "8088", 10); +const SUPERSET_URL = `http://${SUPERSET_HOST}:${SUPERSET_PORT}`; +const ADMIN_USER = process.env.SUPERSET_ADMIN_USERNAME || "admin"; +const ADMIN_PASS = process.env.SUPERSET_ADMIN_PASSWORD || "arcadia1337"; + +// Cache do service token (expira em 50 min) +let serviceToken: string | null = null; +let serviceTokenExpiry = 0; + +async function getServiceToken(): Promise { + if (serviceToken && Date.now() < serviceTokenExpiry) return serviceToken; + + const resp = await fetch(`${SUPERSET_URL}/api/v1/security/login`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + username: ADMIN_USER, + password: ADMIN_PASS, + provider: "db", + refresh: true, + }), + }); + + if (!resp.ok) { + const err = await resp.text().catch(() => ""); + throw new Error(`Falha ao autenticar no Superset: ${resp.status} ${err}`); + } + + const data = await resp.json(); + serviceToken = data.access_token; + serviceTokenExpiry = Date.now() + 50 * 60 * 1000; + return serviceToken!; +} + +async function supersetFetch(path: string, options: RequestInit = {}): Promise { + const token = await getServiceToken(); + const resp = await fetch(`${SUPERSET_URL}${path}`, { + ...options, + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + ...(options.headers || {}), + }, + }); + + if (resp.status === 401) { + // Token expirado — forçar renovação + serviceToken = null; + serviceTokenExpiry = 0; + return supersetFetch(path, options); + } + + if (!resp.ok) { + const err = await resp.text().catch(() => ""); + throw new Error(`Superset API ${resp.status}: ${err}`); + } + + return resp.json().catch(() => ({})); +} + +export function registerSupersetRoutes(app: Express): void { + + // Guest Token — para embedding em qualquer tela React + app.post("/api/superset/guest-token", async (req: Request, res: Response) => { + try { + if (!req.isAuthenticated()) return res.status(401).json({ error: "Not authenticated" }); + + const { dashboardId } = req.body; + if (!dashboardId) return res.status(400).json({ error: "dashboardId obrigatório" }); + + const token = await getServiceToken(); + const user = req.user as any; + + const guestResp = await fetch(`${SUPERSET_URL}/api/v1/security/guest_token/`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + user: { + username: `arcadia_${user?.id || "guest"}`, + first_name: user?.name?.split(" ")[0] || "User", + last_name: user?.name?.split(" ").slice(1).join(" ") || "", + }, + resources: [{ type: "dashboard", id: dashboardId }], + rls: [], + }), + }); + + if (!guestResp.ok) { + const err = await guestResp.text(); + return res.status(502).json({ error: "Falha ao gerar guest token", detail: err }); + } + + const { token: guestToken } = await guestResp.json(); + res.json({ token: guestToken, supersetUrl: "/superset" }); + } catch (err: any) { + res.status(502).json({ error: err.message }); + } + }); + + // Lista dashboards + app.get("/api/superset/dashboards", async (req: Request, res: Response) => { + try { + if (!req.isAuthenticated()) return res.status(401).json({ error: "Not authenticated" }); + const data = await supersetFetch( + "/api/v1/dashboard/?q=(order_column:changed_on_delta_humanized,order_direction:desc,page_size:50)" + ); + res.json(data.result || []); + } catch (err: any) { + res.status(502).json({ error: err.message }); + } + }); + + // Lista charts + app.get("/api/superset/charts", async (req: Request, res: Response) => { + try { + if (!req.isAuthenticated()) return res.status(401).json({ error: "Not authenticated" }); + const data = await supersetFetch("/api/v1/chart/?q=(page_size:50)"); + res.json(data.result || []); + } catch (err: any) { + res.status(502).json({ error: err.message }); + } + }); + + // Health check + app.get("/api/superset/health", async (_req: Request, res: Response) => { + try { + const resp = await fetch(`${SUPERSET_URL}/health`); + res.json({ online: resp.ok, url: SUPERSET_URL }); + } catch { + res.json({ online: false, url: SUPERSET_URL }); + } + }); + + // Manter compatibilidade com rotas antigas do MetaSet + app.get("/api/bi/metaset/health", async (_req: Request, res: Response) => { + try { + const resp = await fetch(`${SUPERSET_URL}/health`); + res.json({ service: "Superset", online: resp.ok }); + } catch { + res.json({ service: "Superset", online: false }); + } + }); + + console.log("[Superset] Rotas registradas em /api/superset/*"); +} From be22bbf77dbc75da7ada4ff47c289f7b7debe92b Mon Sep 17 00:00:00 2001 From: root Date: Tue, 17 Mar 2026 16:13:02 -0300 Subject: [PATCH 05/16] =?UTF-8?q?chore:=20sync=20estado=20completo=20do=20?= =?UTF-8?q?servidor=20=E2=80=94=20.env,=20compose,=20superset,=20routes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Inclui: - .env real de produção (repo privado, auditoria total) - docker-compose.prod.yml atualizado (LiteLLM/LLMFit, Superset) - docker/superset/superset_config.py - client/src/pages/BiWorkspace.tsx - server/superset/routes.ts - .gitignore: remove .env da lista de ignorados Co-Authored-By: Claude Sonnet 4.6 --- .env | 20 +++ .gitignore | 3 - client/src/pages/BiWorkspace.tsx | 8 +- docker-compose.prod.yml | 2 +- docker-compose.prod.yml.bak | 237 +++++++++++++++++++++++++++++ docker/superset/superset_config.py | 11 +- server/superset/routes.ts | 50 ++++++ 7 files changed, 322 insertions(+), 9 deletions(-) create mode 100644 .env create mode 100644 docker-compose.prod.yml.bak diff --git a/.env b/.env new file mode 100644 index 0000000..723e228 --- /dev/null +++ b/.env @@ -0,0 +1,20 @@ +PGPASSWORD=afefb610090de1646944b537f3054715ad5755665c90036c2bcd258c9ab52dd8 +SESSION_SECRET=092a26dcbebd6f7f5e7b9e020bdc4d4e966286c9780ef72e3426f437eb8d608b +SSO_SECRET=fbb87d1c5a14a6623d4e3dcc2b679bd68c46e59afb48dd6abd42b95cac945856 +LITELLM_API_KEY=e396b827cbeb5e77d85cce4eb9da0521114f57a8e799f7aa8febad08d776c07e +WEBUI_SECRET_KEY=76c7fc044f9e8d6e5eb60b67c82832b67e84739da4827ce1fa7c101ae1a055df +DOMAIN=suite.onboardbi.com.br +OLLAMA_BASE_URL=http://ollama-ia1upsekrad96at5hq97e4qa:11434 + +LLMFIT_BASE_URL=http://llmfit:8000 +LLMFIT_API_KEY=key-arcadia + +# ── Superset BI ─────────────────────────────────────────────── +SUPERSET_SECRET_KEY=421274b5ba360778a5398d528116a45ea962d1197e3dfc99f36a372ff1025a63 +SUPERSET_ADMIN_USERNAME=admin +SUPERSET_ADMIN_PASSWORD=arcadia1337 +SUPERSET_ADMIN_EMAIL=engenharia@ometas.com.br +COMPOSE_PROFILES=bi + +SUPERSET_HOST=superset +SUPERSET_PORT=8088 diff --git a/.gitignore b/.gitignore index 688a4b9..0483d65 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,6 @@ node_modules/ dist/ .DS_Store -.env -.env.local -.env.production # Replit specific .cache/ diff --git a/client/src/pages/BiWorkspace.tsx b/client/src/pages/BiWorkspace.tsx index af7341d..9d3ae18 100644 --- a/client/src/pages/BiWorkspace.tsx +++ b/client/src/pages/BiWorkspace.tsx @@ -2919,11 +2919,11 @@ export default function BiWorkspace() {
-

MetaSet - Motor de BI

+

Superset - Motor de BI

Acesso completo para criação manual de consultas SQL, gráficos e dashboards