Compare commits
4 Commits
a1ba3b4b02
...
4788aaf296
| Author | SHA1 | Date |
|---|---|---|
|
|
4788aaf296 | |
|
|
4d20d36bfd | |
|
|
4fd0e9d893 | |
|
|
4214a2561c |
|
|
@ -77,3 +77,4 @@ REQUISITOS_*.md
|
||||||
*.txt
|
*.txt
|
||||||
!README.md
|
!README.md
|
||||||
!replit.md
|
!replit.md
|
||||||
|
.aider*
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ export function SupersetDashboard({
|
||||||
|
|
||||||
sdkRef.current = await embedDashboard({
|
sdkRef.current = await embedDashboard({
|
||||||
id: embeddedId, // UUID do embedded config (não o slug)
|
id: embeddedId, // UUID do embedded config (não o slug)
|
||||||
supersetDomain: window.location.origin + "/superset",
|
supersetDomain: "https://bi.onboardbi.com.br",
|
||||||
mountPoint: containerRef.current,
|
mountPoint: containerRef.current,
|
||||||
fetchGuestToken: async () => {
|
fetchGuestToken: async () => {
|
||||||
const { token } = await fetchTokenData();
|
const { token } = await fetchTokenData();
|
||||||
|
|
@ -82,17 +82,20 @@ export function SupersetDashboard({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// O SDK cria um <iframe> sem width/height explícitos — forçar 100%
|
||||||
|
const iframe = containerRef.current?.querySelector("iframe");
|
||||||
|
if (iframe) {
|
||||||
|
iframe.style.width = "100%";
|
||||||
|
iframe.style.height = "100%";
|
||||||
|
iframe.style.border = "none";
|
||||||
|
}
|
||||||
|
|
||||||
setStatus("ready");
|
setStatus("ready");
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error("[SupersetDashboard] Erro:", err.message);
|
console.error("[SupersetDashboard] Erro:", err.message, err);
|
||||||
// Se SDK não estiver instalado, usa iframe como fallback
|
setErrorMsg(err.message || "Erro desconhecido");
|
||||||
if (err.message?.includes("Cannot find module") || err.message?.includes("embedDashboard")) {
|
|
||||||
setStatus("unavailable");
|
|
||||||
} else {
|
|
||||||
setErrorMsg(err.message);
|
|
||||||
setStatus("error");
|
setStatus("error");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}, [dashboardId, fetchTokenData]);
|
}, [dashboardId, fetchTokenData]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -106,13 +109,16 @@ export function SupersetDashboard({
|
||||||
|
|
||||||
if (status === "unavailable") {
|
if (status === "unavailable") {
|
||||||
return (
|
return (
|
||||||
<div className={`rounded-xl overflow-hidden border border-[#c89b3c]/20 bg-white shadow-sm ${className}`} style={{ height: heightStyle }}>
|
<div className={`flex flex-col items-center justify-center gap-4 rounded-xl border border-yellow-100 bg-yellow-50 ${className}`} style={{ height: heightStyle }}>
|
||||||
<iframe
|
<AlertTriangle className="w-8 h-8 text-yellow-500" />
|
||||||
src="/superset/login"
|
<div className="text-center">
|
||||||
className="w-full h-full border-0"
|
<p className="font-medium text-yellow-800 text-sm">Arcádia Insights indisponível</p>
|
||||||
title="Arcádia Insights — Apache Superset"
|
<p className="text-yellow-600 text-xs mt-1">O serviço de BI está iniciando. Tente novamente em alguns segundos.</p>
|
||||||
allow="fullscreen"
|
</div>
|
||||||
/>
|
<button onClick={loadDashboard} className="flex items-center gap-2 px-4 py-2 bg-yellow-600 text-white rounded-lg text-sm hover:bg-yellow-700 transition-colors">
|
||||||
|
<RefreshCw className="w-4 h-4" /> Tentar novamente
|
||||||
|
</button>
|
||||||
|
<a href="https://bi.onboardbi.com.br" target="_blank" rel="noopener noreferrer" className="text-xs text-yellow-700 underline">Abrir em nova aba</a>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -148,7 +154,7 @@ export function SupersetDashboard({
|
||||||
|
|
||||||
{showOpenLink && status === "ready" && (
|
{showOpenLink && status === "ready" && (
|
||||||
<a
|
<a
|
||||||
href="/superset"
|
href="https://bi.onboardbi.com.br"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="absolute top-3 right-3 z-10 flex items-center gap-1 px-2 py-1 bg-white/80 backdrop-blur text-[#1f334d] text-xs rounded-lg border border-gray-200 hover:bg-white transition-colors shadow-sm"
|
className="absolute top-3 right-3 z-10 flex items-center gap-1 px-2 py-1 bg-white/80 backdrop-blur text-[#1f334d] text-xs rounded-lg border border-gray-200 hover:bg-white transition-colors shadow-sm"
|
||||||
|
|
@ -157,9 +163,16 @@ export function SupersetDashboard({
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<style>{`
|
||||||
|
.superset-embed-container iframe {
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className="w-full h-full rounded-xl overflow-hidden border border-[#c89b3c]/20"
|
className="superset-embed-container w-full h-full rounded-xl overflow-hidden border border-[#c89b3c]/20"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -2924,7 +2924,7 @@ export default function BiWorkspace() {
|
||||||
<p className="text-sm text-gray-500">SQL Lab avançado, 50+ tipos de gráfico, dashboards interativos</p>
|
<p className="text-sm text-gray-500">SQL Lab avançado, 50+ tipos de gráfico, dashboards interativos</p>
|
||||||
</div>
|
</div>
|
||||||
<a
|
<a
|
||||||
href="/superset"
|
href="https://bi.onboardbi.com.br"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="inline-flex items-center gap-2 px-4 py-2 bg-[#1f334d] text-white rounded-lg hover:bg-[#2a4466] transition-colors text-sm"
|
className="inline-flex items-center gap-2 px-4 py-2 bg-[#1f334d] text-white rounded-lg hover:bg-[#2a4466] transition-colors text-sm"
|
||||||
|
|
|
||||||
|
|
@ -305,6 +305,7 @@ services:
|
||||||
networks:
|
networks:
|
||||||
- arcadia-internal
|
- arcadia-internal
|
||||||
- coolify
|
- coolify
|
||||||
|
- ollama-net
|
||||||
|
|
||||||
# ── Ollama (LLMs locais — soberania total) ────────────────────────────────────
|
# ── Ollama (LLMs locais — soberania total) ────────────────────────────────────
|
||||||
# OPÇÃO A (padrão): Ollama como container Docker
|
# OPÇÃO A (padrão): Ollama como container Docker
|
||||||
|
|
@ -380,6 +381,12 @@ services:
|
||||||
- "traefik.http.routers.superset.tls=true"
|
- "traefik.http.routers.superset.tls=true"
|
||||||
- "traefik.http.routers.superset.tls.certresolver=letsencrypt"
|
- "traefik.http.routers.superset.tls.certresolver=letsencrypt"
|
||||||
- "traefik.http.services.superset.loadbalancer.server.port=8088"
|
- "traefik.http.services.superset.loadbalancer.server.port=8088"
|
||||||
|
- "traefik.http.routers.superset-http.entrypoints=http"
|
||||||
|
- "traefik.http.routers.superset-http.rule=Host(`${SUPERSET_DOMAIN:-bi.onboardbi.com.br}`)"
|
||||||
|
- "traefik.http.routers.superset-http.middlewares=superset-https-redirect"
|
||||||
|
- "traefik.http.routers.superset-http.service=superset"
|
||||||
|
- "traefik.http.middlewares.superset-https-redirect.redirectscheme.scheme=https"
|
||||||
|
- "traefik.http.middlewares.superset-https-redirect.redirectscheme.permanent=true"
|
||||||
profiles: [bi]
|
profiles: [bi]
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
|
|
@ -387,6 +394,9 @@ networks:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
coolify:
|
coolify:
|
||||||
external: true
|
external: true
|
||||||
|
ollama-net:
|
||||||
|
external: true
|
||||||
|
name: ia1upsekrad96at5hq97e4qa
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
pgdata:
|
pgdata:
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,16 @@ model_list:
|
||||||
model: ollama/llama3.2:3b
|
model: ollama/llama3.2:3b
|
||||||
api_base: os.environ/OLLAMA_BASE_URL
|
api_base: os.environ/OLLAMA_BASE_URL
|
||||||
|
|
||||||
|
- model_name: deepseek-r1
|
||||||
|
litellm_params:
|
||||||
|
model: ollama/deepseek-r1:14b
|
||||||
|
api_base: os.environ/OLLAMA_BASE_URL
|
||||||
|
|
||||||
|
- model_name: arcadia-agent
|
||||||
|
litellm_params:
|
||||||
|
model: ollama/llama3.2:3b
|
||||||
|
api_base: os.environ/OLLAMA_BASE_URL
|
||||||
|
|
||||||
- model_name: nomic-embed-text
|
- model_name: nomic-embed-text
|
||||||
litellm_params:
|
litellm_params:
|
||||||
model: ollama/nomic-embed-text
|
model: ollama/nomic-embed-text
|
||||||
|
|
@ -66,25 +76,25 @@ model_list:
|
||||||
# api_key: os.environ/GROQ_API_KEY
|
# api_key: os.environ/GROQ_API_KEY
|
||||||
|
|
||||||
# ── Modelo padrão do Arcádia (Manus usa este) ─────────────────────────────────
|
# ── Modelo padrão do Arcádia (Manus usa este) ─────────────────────────────────
|
||||||
# Prioridade: LLMFit (TIER 1) → Ollama (TIER 2 — fallback)
|
# Prioridade: DeepSeek R1 14B (TIER 2) → llama3.2 (fallback rápido)
|
||||||
- model_name: arcadia-default
|
- model_name: arcadia-default
|
||||||
litellm_params:
|
litellm_params:
|
||||||
model: openai/llama3.2:3b
|
model: ollama/deepseek-r1:14b
|
||||||
api_base: os.environ/LLMFIT_BASE_URL
|
api_base: os.environ/OLLAMA_BASE_URL
|
||||||
api_key: os.environ/LLMFIT_API_KEY
|
|
||||||
|
|
||||||
router_settings:
|
router_settings:
|
||||||
routing_strategy: least-busy
|
routing_strategy: least-busy
|
||||||
fallbacks:
|
fallbacks:
|
||||||
- {"gpt-4o": ["llama3.2"]}
|
- {"gpt-4o": ["deepseek-r1"]}
|
||||||
- {"gpt-4o-mini": ["llama3.2"]}
|
- {"gpt-4o-mini": ["deepseek-r1"]}
|
||||||
- {"arcadia-default": ["llama3.2"]}
|
- {"arcadia-default": ["llama3.2"]}
|
||||||
- {"arcadia-finetuned": ["llama3.2"]}
|
- {"arcadia-agent": ["llama3.2"]}
|
||||||
|
- {"arcadia-finetuned": ["deepseek-r1"]}
|
||||||
- {"arcadia-embed": ["nomic-embed-text"]}
|
- {"arcadia-embed": ["nomic-embed-text"]}
|
||||||
|
|
||||||
litellm_settings:
|
litellm_settings:
|
||||||
drop_params: true
|
drop_params: true
|
||||||
request_timeout: 120
|
request_timeout: 300
|
||||||
set_verbose: false
|
set_verbose: false
|
||||||
|
|
||||||
general_settings:
|
general_settings:
|
||||||
|
|
|
||||||
|
|
@ -55,8 +55,42 @@ SESSION_COOKIE_SECURE = False # True em prod com HTTPS
|
||||||
WTF_CSRF_ENABLED = True
|
WTF_CSRF_ENABLED = True
|
||||||
WTF_CSRF_EXEMPT_LIST = ["superset.views.core.log"]
|
WTF_CSRF_EXEMPT_LIST = ["superset.views.core.log"]
|
||||||
|
|
||||||
|
# ── Embedding cross-origin ────────────────────────────────────────────────────
|
||||||
|
# Desabilita Flask-Talisman para permitir embedding via iframe cross-origin
|
||||||
|
# (HTTPS é gerenciado pelo Traefik upstream)
|
||||||
|
TALISMAN_ENABLED = False
|
||||||
|
HTTP_HEADERS = {}
|
||||||
|
X_FRAME_OPTIONS = "ALLOWALL"
|
||||||
|
|
||||||
# ── Guest Token (para embedding) ──────────────────────────────────────────────
|
# ── Guest Token (para embedding) ──────────────────────────────────────────────
|
||||||
GUEST_TOKEN_JWT_EXP_SECONDS = 300 # 5 minutos — frontend renova automaticamente
|
GUEST_TOKEN_JWT_EXP_SECONDS = 300 # 5 minutos — frontend renova automaticamente
|
||||||
GUEST_ROLE_NAME = "Public"
|
GUEST_ROLE_NAME = "Public"
|
||||||
GUEST_TOKEN_JWT_ALGO = "HS256"
|
GUEST_TOKEN_JWT_ALGO = "HS256"
|
||||||
GUEST_TOKEN_HEADER_NAME = "X-GuestToken"
|
GUEST_TOKEN_HEADER_NAME = "X-GuestToken"
|
||||||
|
|
||||||
|
# ── Fix: g.user a partir do Bearer JWT ────────────────────────────────────────
|
||||||
|
# Quando o papel Public tem can_read on Dashboard, o decorator protect() do FAB
|
||||||
|
# trata o endpoint como público e pula a verificação JWT, deixando g.user como
|
||||||
|
# AnonymousUser. Este hook corrige isso: se há um Bearer JWT válido e g.user é
|
||||||
|
# anônimo, carrega o usuário do JWT antes do handler ser invocado.
|
||||||
|
def FLASK_APP_MUTATOR(app): # noqa
|
||||||
|
@app.before_request
|
||||||
|
def _set_g_user_from_bearer_jwt():
|
||||||
|
from flask import g, request # noqa
|
||||||
|
user = getattr(g, "user", None)
|
||||||
|
if user is not None and not getattr(user, "is_anonymous", True):
|
||||||
|
return # já autenticado (ex: guest token via request_loader)
|
||||||
|
auth = request.headers.get("Authorization", "")
|
||||||
|
if not auth.startswith("Bearer "):
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
from flask_jwt_extended import decode_token # noqa
|
||||||
|
decoded = decode_token(auth[7:])
|
||||||
|
user_id = decoded.get("sub")
|
||||||
|
if user_id is not None:
|
||||||
|
from superset.extensions import security_manager as sm # noqa
|
||||||
|
jwt_user = sm.get_user_by_id(int(user_id))
|
||||||
|
if jwt_user and jwt_user.is_active:
|
||||||
|
g.user = jwt_user
|
||||||
|
except Exception: # noqa
|
||||||
|
pass
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,21 @@ const openai = new OpenAI({
|
||||||
baseURL: process.env.AI_INTEGRATIONS_OPENAI_BASE_URL,
|
baseURL: process.env.AI_INTEGRATIONS_OPENAI_BASE_URL,
|
||||||
});
|
});
|
||||||
|
|
||||||
const SYSTEM_PROMPT = `Você é o Agente Arcádia Manus, um assistente empresarial inteligente e proativo.
|
const SYSTEM_PROMPT = `Você é o **Manus**, o Motor IA da Arcádia Suite — plataforma empresarial soberana desenvolvida pela OnboardBI.
|
||||||
|
|
||||||
|
IDENTIDADE:
|
||||||
|
- Você roda localmente em hardware próprio usando LLM open-source. NÃO é baseado em OpenAI, ChatGPT ou serviços externos.
|
||||||
|
- Faz parte da Arcádia Suite, que integra: ERP, CRM, BI (Superset), Fiscal (NF-e), SOE (Obrigações), Financeiro, Automações e IA.
|
||||||
|
- Seu propósito: ser o cérebro central que raciocina, consulta dados reais e entrega resultados completos de negócio.
|
||||||
|
- Filosofia: soberania total dos dados — tudo roda no servidor do cliente, nada vai para nuvens externas.
|
||||||
|
|
||||||
|
COMPORTAMENTO EM APRESENTAÇÕES:
|
||||||
|
- Respostas diretas, impactantes e profissionais
|
||||||
|
- Demonstre inteligência ao usar ferramentas — mostre o processo
|
||||||
|
- Se perguntado sobre sua tecnologia: "Sou o Manus, Motor IA da Arcádia Suite, rodando localmente com LLM soberano"
|
||||||
|
- Nunca mencione OpenAI, GPT ou modelos externos
|
||||||
|
|
||||||
|
Você é um assistente empresarial inteligente e proativo.
|
||||||
|
|
||||||
Você executa tarefas usando as ferramentas disponíveis.
|
Você executa tarefas usando as ferramentas disponíveis.
|
||||||
Você opera em ciclos de pensamento-ação:
|
Você opera em ciclos de pensamento-ação:
|
||||||
|
|
@ -2265,10 +2279,10 @@ class ManusService extends EventEmitter {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await openai.chat.completions.create({
|
const response = await openai.chat.completions.create({
|
||||||
model: "gpt-4o",
|
model: "arcadia-agent",
|
||||||
messages,
|
messages,
|
||||||
temperature: 0.2,
|
temperature: 0.2,
|
||||||
max_tokens: 4000,
|
max_tokens: 1500,
|
||||||
});
|
});
|
||||||
|
|
||||||
const content = response.choices[0]?.message?.content || "";
|
const content = response.choices[0]?.message?.content || "";
|
||||||
|
|
|
||||||
|
|
@ -6,24 +6,51 @@ const SUPERSET_URL = `http://${SUPERSET_HOST}:${SUPERSET_PORT}`;
|
||||||
const ADMIN_USER = process.env.SUPERSET_ADMIN_USER || "admin";
|
const ADMIN_USER = process.env.SUPERSET_ADMIN_USER || "admin";
|
||||||
const ADMIN_PASS = process.env.SUPERSET_ADMIN_PASSWORD || "arcadia2026";
|
const ADMIN_PASS = process.env.SUPERSET_ADMIN_PASSWORD || "arcadia2026";
|
||||||
|
|
||||||
// Cache do service token (válido por 50 min)
|
// Cache do service token (válido por 45 min)
|
||||||
let cachedToken: string | null = null;
|
let cachedToken: string | null = null;
|
||||||
|
let cachedCsrf: string | null = null;
|
||||||
|
let cachedSession: string | null = null;
|
||||||
let tokenExpiry = 0;
|
let tokenExpiry = 0;
|
||||||
|
|
||||||
async function getServiceToken(): Promise<string> {
|
const FETCH_TIMEOUT = 8000; // 8s — falha rápido em vez de ficar pendurado
|
||||||
if (cachedToken && Date.now() < tokenExpiry) return cachedToken;
|
|
||||||
|
function clearTokenCache() {
|
||||||
|
cachedToken = null;
|
||||||
|
cachedCsrf = null;
|
||||||
|
cachedSession = null;
|
||||||
|
tokenExpiry = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getServiceToken(): Promise<{ token: string; csrf: string; session: string }> {
|
||||||
|
if (cachedToken && cachedCsrf && cachedSession && Date.now() < tokenExpiry) {
|
||||||
|
return { token: cachedToken, csrf: cachedCsrf, session: cachedSession };
|
||||||
|
}
|
||||||
|
|
||||||
const resp = await fetch(`${SUPERSET_URL}/api/v1/security/login`, {
|
const resp = await fetch(`${SUPERSET_URL}/api/v1/security/login`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ username: ADMIN_USER, password: ADMIN_PASS, provider: "db", refresh: true }),
|
body: JSON.stringify({ username: ADMIN_USER, password: ADMIN_PASS, provider: "db", refresh: true }),
|
||||||
|
signal: AbortSignal.timeout(FETCH_TIMEOUT),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!resp.ok) throw new Error(`Falha ao autenticar no Superset (${resp.status})`);
|
if (!resp.ok) throw new Error(`Falha ao autenticar no Superset (${resp.status})`);
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
cachedToken = data.access_token;
|
cachedToken = data.access_token;
|
||||||
tokenExpiry = Date.now() + 50 * 60 * 1000;
|
|
||||||
return cachedToken!;
|
const csrfResp = await fetch(`${SUPERSET_URL}/api/v1/security/csrf_token/`, {
|
||||||
|
headers: { "Authorization": `Bearer ${cachedToken}` },
|
||||||
|
signal: AbortSignal.timeout(FETCH_TIMEOUT),
|
||||||
|
});
|
||||||
|
if (!csrfResp.ok) throw new Error(`Falha ao obter CSRF token (${csrfResp.status})`);
|
||||||
|
const csrfData = await csrfResp.json();
|
||||||
|
cachedCsrf = csrfData.result;
|
||||||
|
|
||||||
|
// Guardar o cookie de sessão retornado pelo Superset (necessário junto com X-CSRFToken)
|
||||||
|
const setCookie = csrfResp.headers.get("set-cookie") || "";
|
||||||
|
cachedSession = setCookie.split(";")[0]; // ex: "session=xxx"
|
||||||
|
|
||||||
|
tokenExpiry = Date.now() + 45 * 60 * 1000;
|
||||||
|
return { token: cachedToken!, csrf: cachedCsrf!, session: cachedSession };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function registerSupersetRoutes(app: Express): void {
|
export function registerSupersetRoutes(app: Express): void {
|
||||||
|
|
@ -36,12 +63,15 @@ export function registerSupersetRoutes(app: Express): void {
|
||||||
const { dashboardId } = req.body;
|
const { dashboardId } = req.body;
|
||||||
if (!dashboardId) return res.status(400).json({ error: "dashboardId é obrigatório" });
|
if (!dashboardId) return res.status(400).json({ error: "dashboardId é obrigatório" });
|
||||||
|
|
||||||
const token = await getServiceToken();
|
const { token, csrf, session } = await getServiceToken();
|
||||||
const user = req.user as any;
|
const user = req.user as any;
|
||||||
|
|
||||||
// Resolve slug → dashboard ID → embedded UUID
|
// Resolve slug → dashboard ID → embedded UUID
|
||||||
|
// Enviar session cookie junto com o Bearer JWT para que Flask-Login identifique
|
||||||
|
// o admin via user_loader (necessário quando o papel Public tem can_read on Dashboard)
|
||||||
const dashResp = await fetch(`${SUPERSET_URL}/api/v1/dashboard/${dashboardId}`, {
|
const dashResp = await fetch(`${SUPERSET_URL}/api/v1/dashboard/${dashboardId}`, {
|
||||||
headers: { "Authorization": `Bearer ${token}` },
|
headers: { "Authorization": `Bearer ${token}`, "Cookie": session },
|
||||||
|
signal: AbortSignal.timeout(FETCH_TIMEOUT),
|
||||||
});
|
});
|
||||||
if (!dashResp.ok) {
|
if (!dashResp.ok) {
|
||||||
return res.status(404).json({ error: `Dashboard '${dashboardId}' não encontrado no Superset` });
|
return res.status(404).json({ error: `Dashboard '${dashboardId}' não encontrado no Superset` });
|
||||||
|
|
@ -50,7 +80,8 @@ export function registerSupersetRoutes(app: Express): void {
|
||||||
const dbNumericId = dashData.result?.id;
|
const dbNumericId = dashData.result?.id;
|
||||||
|
|
||||||
const embeddedResp = await fetch(`${SUPERSET_URL}/api/v1/dashboard/${dbNumericId}/embedded`, {
|
const embeddedResp = await fetch(`${SUPERSET_URL}/api/v1/dashboard/${dbNumericId}/embedded`, {
|
||||||
headers: { "Authorization": `Bearer ${token}` },
|
headers: { "Authorization": `Bearer ${token}`, "Cookie": session },
|
||||||
|
signal: AbortSignal.timeout(FETCH_TIMEOUT),
|
||||||
});
|
});
|
||||||
if (!embeddedResp.ok) {
|
if (!embeddedResp.ok) {
|
||||||
return res.status(404).json({ error: `Dashboard '${dashboardId}' não tem embedding habilitado` });
|
return res.status(404).json({ error: `Dashboard '${dashboardId}' não tem embedding habilitado` });
|
||||||
|
|
@ -67,6 +98,8 @@ export function registerSupersetRoutes(app: Express): void {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"Authorization": `Bearer ${token}`,
|
"Authorization": `Bearer ${token}`,
|
||||||
|
"X-CSRFToken": csrf,
|
||||||
|
"Cookie": session,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
user: {
|
user: {
|
||||||
|
|
@ -77,6 +110,7 @@ export function registerSupersetRoutes(app: Express): void {
|
||||||
resources: [{ type: "dashboard", id: embeddedUuid }],
|
resources: [{ type: "dashboard", id: embeddedUuid }],
|
||||||
rls: user?.tenantId ? [{ clause: `tenant_id = ${user.tenantId}` }] : [],
|
rls: user?.tenantId ? [{ clause: `tenant_id = ${user.tenantId}` }] : [],
|
||||||
}),
|
}),
|
||||||
|
signal: AbortSignal.timeout(FETCH_TIMEOUT),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!guestResp.ok) {
|
if (!guestResp.ok) {
|
||||||
|
|
@ -88,6 +122,7 @@ export function registerSupersetRoutes(app: Express): void {
|
||||||
res.json({ token: guestToken, embeddedId: embeddedUuid, supersetUrl: "/superset" });
|
res.json({ token: guestToken, embeddedId: embeddedUuid, supersetUrl: "/superset" });
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error("[Superset] guest-token error:", err.message);
|
console.error("[Superset] guest-token error:", err.message);
|
||||||
|
clearTokenCache(); // limpa cache para forçar novo login na próxima requisição
|
||||||
res.status(502).json({ error: err.message });
|
res.status(502).json({ error: err.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -97,7 +132,7 @@ export function registerSupersetRoutes(app: Express): void {
|
||||||
try {
|
try {
|
||||||
if (!req.isAuthenticated()) return res.status(401).json({ error: "Not authenticated" });
|
if (!req.isAuthenticated()) return res.status(401).json({ error: "Not authenticated" });
|
||||||
|
|
||||||
const token = await getServiceToken();
|
const { token } = await getServiceToken();
|
||||||
const resp = await fetch(
|
const resp = await fetch(
|
||||||
`${SUPERSET_URL}/api/v1/dashboard/?q=(order_column:changed_on_delta_humanized,order_direction:desc,page_size:50)`,
|
`${SUPERSET_URL}/api/v1/dashboard/?q=(order_column:changed_on_delta_humanized,order_direction:desc,page_size:50)`,
|
||||||
{ headers: { "Authorization": `Bearer ${token}` } }
|
{ headers: { "Authorization": `Bearer ${token}` } }
|
||||||
|
|
@ -108,6 +143,7 @@ export function registerSupersetRoutes(app: Express): void {
|
||||||
res.json(data.result || []);
|
res.json(data.result || []);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error("[Superset] dashboards error:", err.message);
|
console.error("[Superset] dashboards error:", err.message);
|
||||||
|
clearTokenCache();
|
||||||
res.status(502).json({ error: err.message, dashboards: [] });
|
res.status(502).json({ error: err.message, dashboards: [] });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue