diff --git a/.env.example b/.env.example index 656b051..359d87e 100644 --- a/.env.example +++ b/.env.example @@ -68,9 +68,15 @@ PLUS_URL=http://localhost:8080 PLUS_PORT=8080 PLUS_API_TOKEN= -# ── Superset (BI avançado) ──────────────────────────────────────────────────── -SUPERSET_SECRET_KEY=troque-por-string-aleatoria-segura +# ── Superset (BI avançado — Arcádia Insights) ──────────────────────────────── +SUPERSET_HOST=superset SUPERSET_PORT=8088 +SUPERSET_SECRET_KEY=troque-por-string-aleatoria-segura # openssl rand -hex 32 +SUPERSET_ADMIN_USER=admin +SUPERSET_ADMIN_EMAIL=admin@arcadia.app +SUPERSET_ADMIN_PASSWORD=troque-senha-forte-em-prod +# URL do banco Arcádia para o Superset acessar os dados (idealmente read-only) +ARCADIA_DATABASE_URL=postgresql://arcadia:arcadia123@db:5432/arcadia # ── Redis ───────────────────────────────────────────────────────────────────── REDIS_URL=redis://localhost:6379 @@ -78,8 +84,20 @@ REDIS_URL=redis://localhost:6379 # ── Domínio (produção) ──────────────────────────────────────────────────────── DOMAIN=seudominio.com.br -# ── Integrações externas (opcional) ────────────────────────────────────────── -# ERPNext +# ── ERPNext ──────────────────────────────────────────────────────────────────── +# Modo 1 — ERPNext em container Docker (perfil erpnext) +# Sobe com: docker compose --profile erpnext up +# Proxy ativado automaticamente em DOCKER_MODE=true +ERPNEXT_PROXY_ENABLED=false # true para forçar proxy mesmo fora do Docker +ERPNEXT_CONTAINER_HOST=erpnext # nome do serviço no docker-compose +ERPNEXT_CONTAINER_PORT=8080 +ERPNEXT_DB_ROOT_PASSWORD=erpnext-root-2026 # senha root do MariaDB +ERPNEXT_DB_PASSWORD=frappe2026 # senha do usuário frappe +ERPNEXT_ADMIN_PASSWORD=admin2026 # senha do Administrator no site + +# Modo 2 — ERPNext externo (instância existente) +# Em Docker: ERPNEXT_URL=http://erpnext:8080 (aponta pro container) +# Externo: ERPNEXT_URL=https://meuerpnext.com ERPNEXT_URL= ERPNEXT_API_KEY= ERPNEXT_API_SECRET= diff --git a/client/src/App.tsx b/client/src/App.tsx index ea75205..f4a16aa 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,5 +1,5 @@ -import { Switch, Route } from "wouter"; -import { lazy, Suspense } from "react"; +import { Switch, Route, useLocation } from "wouter"; +import { lazy, Suspense, useEffect } from "react"; import { queryClient } from "./lib/queryClient"; import { QueryClientProvider } from "@tanstack/react-query"; import { Toaster } from "@/components/ui/toaster"; @@ -121,7 +121,7 @@ function Router() { - + { const [, nav] = useLocation(); useEffect(() => nav("/soe"), []); return null; }} /> diff --git a/client/src/components/SupersetDashboard.tsx b/client/src/components/SupersetDashboard.tsx new file mode 100644 index 0000000..c465bce --- /dev/null +++ b/client/src/components/SupersetDashboard.tsx @@ -0,0 +1,160 @@ +import { useEffect, useRef, useState, useCallback } from "react"; +import { Loader2, AlertTriangle, ExternalLink, RefreshCw } from "lucide-react"; + +interface SupersetDashboardProps { + dashboardId: string; + height?: string | number; + className?: string; + showOpenLink?: boolean; +} + +/** + * Componente reutilizável para embedar dashboards do Apache Superset (Arcádia Insights) + * em QUALQUER tela do Arcádia Suite via Guest Token JWT. + * + * Uso: + * + * + */ +export function SupersetDashboard({ + dashboardId, + height = "calc(100vh - 320px)", + className = "", + showOpenLink = true, +}: SupersetDashboardProps) { + const containerRef = useRef(null); + const sdkRef = useRef(null); + const [status, setStatus] = useState<"loading" | "ready" | "error" | "unavailable">("loading"); + const [errorMsg, setErrorMsg] = useState(""); + + const fetchGuestToken = useCallback(async (): Promise => { + const resp = await fetch("/api/superset/guest-token", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ dashboardId }), + credentials: "include", + }); + if (!resp.ok) { + const err = await resp.json().catch(() => ({ error: "Erro desconhecido" })); + throw new Error(err.error || `HTTP ${resp.status}`); + } + const { token } = await resp.json(); + return token; + }, [dashboardId]); + + const loadDashboard = useCallback(async () => { + if (!containerRef.current) return; + setStatus("loading"); + setErrorMsg(""); + + try { + // Verifica se Superset está disponível + const health = await fetch("/api/superset/health", { credentials: "include" }); + const healthData = await health.json(); + if (!healthData.online) { + setStatus("unavailable"); + return; + } + + // Importa SDK dinamicamente (só quando necessário) + const { embedDashboard } = await import("@superset-ui/embedded-sdk"); + + // Limpa embedding anterior se houver + if (sdkRef.current) { + containerRef.current.innerHTML = ""; + } + + sdkRef.current = await embedDashboard({ + id: dashboardId, + supersetDomain: window.location.origin + "/superset", + mountPoint: containerRef.current, + fetchGuestToken, + dashboardUiConfig: { + hideTitle: true, + hideChartControls: false, + filters: { visible: true, expanded: false }, + }, + }); + + setStatus("ready"); + } catch (err: any) { + console.error("[SupersetDashboard] Erro:", err.message); + // 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"); + } + } + }, [dashboardId, fetchGuestToken]); + + useEffect(() => { + loadDashboard(); + return () => { + if (containerRef.current) containerRef.current.innerHTML = ""; + }; + }, [loadDashboard]); + + const heightStyle = typeof height === "number" ? `${height}px` : height; + + if (status === "unavailable") { + return ( +
+