Compare commits

..

No commits in common. "4788aaf296cbc2a2825d1c5fca9eda4c834632d8" and "a1ba3b4b02f757d0586e146a072129425feed07e" have entirely different histories.

8 changed files with 39 additions and 157 deletions

1
.gitignore vendored
View File

@ -77,4 +77,3 @@ REQUISITOS_*.md
*.txt *.txt
!README.md !README.md
!replit.md !replit.md
.aider*

View File

@ -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: "https://bi.onboardbi.com.br", supersetDomain: window.location.origin + "/superset",
mountPoint: containerRef.current, mountPoint: containerRef.current,
fetchGuestToken: async () => { fetchGuestToken: async () => {
const { token } = await fetchTokenData(); const { token } = await fetchTokenData();
@ -82,20 +82,17 @@ 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, err); console.error("[SupersetDashboard] Erro:", err.message);
setErrorMsg(err.message || "Erro desconhecido"); // Se SDK não estiver instalado, usa iframe como fallback
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(() => {
@ -109,16 +106,13 @@ export function SupersetDashboard({
if (status === "unavailable") { if (status === "unavailable") {
return ( return (
<div className={`flex flex-col items-center justify-center gap-4 rounded-xl border border-yellow-100 bg-yellow-50 ${className}`} style={{ height: heightStyle }}> <div className={`rounded-xl overflow-hidden border border-[#c89b3c]/20 bg-white shadow-sm ${className}`} style={{ height: heightStyle }}>
<AlertTriangle className="w-8 h-8 text-yellow-500" /> <iframe
<div className="text-center"> src="/superset/login"
<p className="font-medium text-yellow-800 text-sm">Arcádia Insights indisponível</p> className="w-full h-full border-0"
<p className="text-yellow-600 text-xs mt-1">O serviço de BI está iniciando. Tente novamente em alguns segundos.</p> title="Arcádia Insights — Apache Superset"
</div> allow="fullscreen"
<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>
); );
} }
@ -154,7 +148,7 @@ export function SupersetDashboard({
{showOpenLink && status === "ready" && ( {showOpenLink && status === "ready" && (
<a <a
href="https://bi.onboardbi.com.br" href="/superset"
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"
@ -163,16 +157,9 @@ 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="superset-embed-container w-full h-full rounded-xl overflow-hidden border border-[#c89b3c]/20" className="w-full h-full rounded-xl overflow-hidden border border-[#c89b3c]/20"
/> />
</div> </div>
); );

View File

@ -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="https://bi.onboardbi.com.br" href="/superset"
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"

View File

@ -305,7 +305,6 @@ 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
@ -381,12 +380,6 @@ 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:
@ -394,9 +387,6 @@ networks:
driver: bridge driver: bridge
coolify: coolify:
external: true external: true
ollama-net:
external: true
name: ia1upsekrad96at5hq97e4qa
volumes: volumes:
pgdata: pgdata:

View File

@ -31,16 +31,6 @@ 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
@ -76,25 +66,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: DeepSeek R1 14B (TIER 2) → llama3.2 (fallback rápido) # Prioridade: LLMFit (TIER 1) → Ollama (TIER 2 — fallback)
- model_name: arcadia-default - model_name: arcadia-default
litellm_params: litellm_params:
model: ollama/deepseek-r1:14b model: openai/llama3.2:3b
api_base: os.environ/OLLAMA_BASE_URL api_base: os.environ/LLMFIT_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": ["deepseek-r1"]} - {"gpt-4o": ["llama3.2"]}
- {"gpt-4o-mini": ["deepseek-r1"]} - {"gpt-4o-mini": ["llama3.2"]}
- {"arcadia-default": ["llama3.2"]} - {"arcadia-default": ["llama3.2"]}
- {"arcadia-agent": ["llama3.2"]} - {"arcadia-finetuned": ["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: 300 request_timeout: 120
set_verbose: false set_verbose: false
general_settings: general_settings:

View File

@ -55,42 +55,8 @@ 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

View File

@ -14,21 +14,7 @@ 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 **Manus**, o Motor IA da Arcádia Suite — plataforma empresarial soberana desenvolvida pela OnboardBI. const SYSTEM_PROMPT = `Você é o Agente Arcádia Manus, um assistente empresarial inteligente e proativo.
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:
@ -2279,10 +2265,10 @@ class ManusService extends EventEmitter {
try { try {
const response = await openai.chat.completions.create({ const response = await openai.chat.completions.create({
model: "arcadia-agent", model: "gpt-4o",
messages, messages,
temperature: 0.2, temperature: 0.2,
max_tokens: 1500, max_tokens: 4000,
}); });
const content = response.choices[0]?.message?.content || ""; const content = response.choices[0]?.message?.content || "";

View File

@ -6,51 +6,24 @@ 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 45 min) // Cache do service token (válido por 50 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;
const FETCH_TIMEOUT = 8000; // 8s — falha rápido em vez de ficar pendurado async function getServiceToken(): Promise<string> {
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;
const csrfResp = await fetch(`${SUPERSET_URL}/api/v1/security/csrf_token/`, { return cachedToken!;
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 {
@ -63,15 +36,12 @@ 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, csrf, session } = await getServiceToken(); const token = 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}`, "Cookie": session }, headers: { "Authorization": `Bearer ${token}` },
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` });
@ -80,8 +50,7 @@ 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}`, "Cookie": session }, headers: { "Authorization": `Bearer ${token}` },
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` });
@ -98,8 +67,6 @@ 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: {
@ -110,7 +77,6 @@ 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) {
@ -122,7 +88,6 @@ 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 });
} }
}); });
@ -132,7 +97,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}` } }
@ -143,7 +108,6 @@ 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: [] });
} }
}); });