feat(bi): migração MetaSet → Apache Superset (Fase 1–4 completas)

- BiWorkspace: tab Advanced substituída por SupersetDashboard (financial-overview)
- Financeiro: nova tab "Análise BI" com SupersetDashboard (financial-overview)
- Contabil: nova tab "Análise BI" com SupersetDashboard (dre-mensal)
- SOE: card Arcádia Insights no dashboard (executive-summary)
- Home: seção full-width Arcádia Insights (executive-summary)
- docker-compose.yml dev: serviço superset adicionado com profile [bi]
- .env.example: documentadas variáveis SUPERSET_* e remoção de METABASE_*
- server/superset/routes.ts: corrigido payload RLS (removido type:"BASE_ROLE" inválido)

Arquivos metaset/metabase mantidos: ainda usados por server/manus e server/autonomous.
Próximos passos: subir container, criar dashboards base, testar health/guest-token.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jonas Pacheco 2026-03-19 14:08:03 -03:00
parent 4ecc8846bf
commit 953567fa47
8 changed files with 90 additions and 22 deletions

View File

@ -53,3 +53,18 @@ PEOPLE_PORT=8004
# ---- Protocolos MCP/A2A (opcional) ---- # ---- Protocolos MCP/A2A (opcional) ----
PROTOCOL_API_KEYS=sua-api-key-aqui PROTOCOL_API_KEYS=sua-api-key-aqui
# ---- Apache Superset — BI Visual (opcional, ativar com --profile bi) ----
# Gerar chave: openssl rand -hex 32
SUPERSET_SECRET_KEY=gere-com-openssl-rand-hex-32
SUPERSET_ADMIN_USER=admin
SUPERSET_ADMIN_EMAIL=admin@arcadia.app
SUPERSET_ADMIN_PASSWORD=troque-em-producao
# Domínio público do Superset (apenas prod/Traefik)
SUPERSET_DOMAIN=bi.seudominio.com.br
# Remover do .env se existirem (não são mais necessários):
# METABASE_HOST
# METABASE_PORT
# METASET_ADMIN_EMAIL
# METASET_ADMIN_PASSWORD

View File

@ -80,6 +80,7 @@ import {
ResponsiveContainer, ResponsiveContainer,
Cell, Cell,
} from "recharts"; } from "recharts";
import { SupersetDashboard } from "@/components/SupersetDashboard";
interface BiStats { interface BiStats {
dataSources: number; dataSources: number;
@ -2890,7 +2891,7 @@ export default function BiWorkspace() {
<Layers className="w-4 h-4 mr-2" /> Staging <Layers className="w-4 h-4 mr-2" /> Staging
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="advanced" className="data-[state=active]:bg-[#c89b3c] data-[state=active]:text-[#1f334d] text-white/70"> <TabsTrigger value="advanced" className="data-[state=active]:bg-[#c89b3c] data-[state=active]:text-[#1f334d] text-white/70">
<Settings className="w-4 h-4 mr-2" /> MetaSet <Settings className="w-4 h-4 mr-2" /> Insights
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
@ -2919,28 +2920,20 @@ export default function BiWorkspace() {
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h2 className="text-lg font-semibold text-[#1f334d]">MetaSet - Motor de BI</h2> <h2 className="text-lg font-semibold text-[#1f334d]">Arcádia Insights Apache Superset</h2>
<p className="text-sm text-gray-500">Acesso completo para criação manual de consultas SQL, gráficos e dashboards</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="/api/bi/metaset/autologin" 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"
data-testid="link-open-metaset-external" data-testid="link-open-superset-external"
> >
<ArrowRight className="w-4 h-4" /> Abrir em Nova Aba <ArrowRight className="w-4 h-4" /> Abrir em Nova Aba
</a> </a>
</div> </div>
<div className="rounded-xl overflow-hidden border border-[#c89b3c]/20 bg-white shadow-sm" style={{ height: 'calc(100vh - 320px)' }}> <SupersetDashboard dashboardId="executive-summary" height="calc(100vh - 320px)" />
<iframe
src="/api/bi/metaset/autologin"
className="w-full h-full border-0"
title="MetaSet - Arcádia Insights"
data-testid="iframe-metaset-advanced"
allow="fullscreen"
/>
</div>
</div> </div>
</TabsContent> </TabsContent>
</Tabs> </Tabs>

View File

@ -1,4 +1,5 @@
import { useState } from "react"; import { useState } from "react";
import { SupersetDashboard } from "@/components/SupersetDashboard";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { import {
BookOpen, BookOpen,
@ -391,12 +392,13 @@ export default function Contabil() {
</div> </div>
<Tabs value={activeTab} onValueChange={setActiveTab}> <Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid w-full grid-cols-5"> <TabsList className="grid w-full grid-cols-6">
<TabsTrigger value="dashboard" data-testid="tab-dashboard">Dashboard</TabsTrigger> <TabsTrigger value="dashboard" data-testid="tab-dashboard">Dashboard</TabsTrigger>
<TabsTrigger value="plano" data-testid="tab-plano">Plano de Contas</TabsTrigger> <TabsTrigger value="plano" data-testid="tab-plano">Plano de Contas</TabsTrigger>
<TabsTrigger value="lancamentos" data-testid="tab-lancamentos">Lançamentos</TabsTrigger> <TabsTrigger value="lancamentos" data-testid="tab-lancamentos">Lançamentos</TabsTrigger>
<TabsTrigger value="centros" data-testid="tab-centros">Centros de Custo</TabsTrigger> <TabsTrigger value="centros" data-testid="tab-centros">Centros de Custo</TabsTrigger>
<TabsTrigger value="sped" data-testid="tab-sped">SPED ECD</TabsTrigger> <TabsTrigger value="sped" data-testid="tab-sped">SPED ECD</TabsTrigger>
<TabsTrigger value="analise" data-testid="tab-analise">Análise BI</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="dashboard" className="space-y-4"> <TabsContent value="dashboard" className="space-y-4">
@ -785,6 +787,9 @@ export default function Contabil() {
</CardContent> </CardContent>
</Card> </Card>
</TabsContent> </TabsContent>
<TabsContent value="analise">
<SupersetDashboard dashboardId="dre-mensal" />
</TabsContent>
</Tabs> </Tabs>
<Dialog open={showContaDialog} onOpenChange={setShowContaDialog}> <Dialog open={showContaDialog} onOpenChange={setShowContaDialog}>

View File

@ -1,4 +1,5 @@
import { BrowserFrame } from "@/components/Browser/BrowserFrame"; import { BrowserFrame } from "@/components/Browser/BrowserFrame";
import { SupersetDashboard } from "@/components/SupersetDashboard";
import { useState } from "react"; import { useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@ -40,7 +41,7 @@ import {
import { format } from "date-fns"; import { format } from "date-fns";
import { ptBR } from "date-fns/locale"; import { ptBR } from "date-fns/locale";
type TabType = "dashboard" | "payables" | "receivables" | "accounts" | "transactions" | "settings"; type TabType = "dashboard" | "payables" | "receivables" | "accounts" | "transactions" | "settings" | "analise";
interface BankAccount { interface BankAccount {
id: number; id: number;
@ -975,13 +976,14 @@ export default function Financeiro() {
</div> </div>
<Tabs value={activeTab} onValueChange={(v) => setActiveTab(v as TabType)} className="space-y-4"> <Tabs value={activeTab} onValueChange={(v) => setActiveTab(v as TabType)} className="space-y-4">
<TabsList className="grid grid-cols-6 w-full max-w-4xl"> <TabsList className="grid grid-cols-7 w-full max-w-4xl">
<TabsTrigger value="dashboard" data-testid="tab-dashboard">Dashboard</TabsTrigger> <TabsTrigger value="dashboard" data-testid="tab-dashboard">Dashboard</TabsTrigger>
<TabsTrigger value="payables" data-testid="tab-payables">A Pagar</TabsTrigger> <TabsTrigger value="payables" data-testid="tab-payables">A Pagar</TabsTrigger>
<TabsTrigger value="receivables" data-testid="tab-receivables">A Receber</TabsTrigger> <TabsTrigger value="receivables" data-testid="tab-receivables">A Receber</TabsTrigger>
<TabsTrigger value="accounts" data-testid="tab-accounts">Contas</TabsTrigger> <TabsTrigger value="accounts" data-testid="tab-accounts">Contas</TabsTrigger>
<TabsTrigger value="transactions" data-testid="tab-transactions">Extrato</TabsTrigger> <TabsTrigger value="transactions" data-testid="tab-transactions">Extrato</TabsTrigger>
<TabsTrigger value="settings" data-testid="tab-settings">Configurações</TabsTrigger> <TabsTrigger value="settings" data-testid="tab-settings">Configurações</TabsTrigger>
<TabsTrigger value="analise" data-testid="tab-analise">Análise BI</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="dashboard">{renderDashboard()}</TabsContent> <TabsContent value="dashboard">{renderDashboard()}</TabsContent>
@ -990,6 +992,9 @@ export default function Financeiro() {
<TabsContent value="accounts">{renderAccounts()}</TabsContent> <TabsContent value="accounts">{renderAccounts()}</TabsContent>
<TabsContent value="transactions">{renderTransactions()}</TabsContent> <TabsContent value="transactions">{renderTransactions()}</TabsContent>
<TabsContent value="settings">{renderSettings()}</TabsContent> <TabsContent value="settings">{renderSettings()}</TabsContent>
<TabsContent value="analise">
<SupersetDashboard dashboardId="financial-overview" />
</TabsContent>
</Tabs> </Tabs>
<Dialog open={showAccountDialog} onOpenChange={setShowAccountDialog}> <Dialog open={showAccountDialog} onOpenChange={setShowAccountDialog}>

View File

@ -1,4 +1,5 @@
import { BrowserFrame } from "@/components/Browser/BrowserFrame"; import { BrowserFrame } from "@/components/Browser/BrowserFrame";
import { SupersetDashboard } from "@/components/SupersetDashboard";
import { import {
Search, Grid, Settings, FileText, Plus, Star, Search, Grid, Settings, FileText, Plus, Star,
ChevronRight, Bell, Clock, CheckSquare, ChevronRight, Bell, Clock, CheckSquare,
@ -1170,6 +1171,15 @@ export default function Home() {
)} )}
</div> </div>
</div> </div>
{/* Arcádia Insights — Dashboard executivo */}
<div className="mt-6 px-6 pb-6">
<div className="flex items-center justify-between mb-3">
<h2 className="font-semibold text-slate-800">Arcádia Insights</h2>
<a href="/bi" className="text-xs text-indigo-500 hover:underline">Abrir BI completo </a>
</div>
<SupersetDashboard dashboardId="executive-summary" height={400} />
</div>
</div> </div>
</main> </main>

View File

@ -1,4 +1,5 @@
import { BrowserFrame } from "@/components/Browser/BrowserFrame"; import { BrowserFrame } from "@/components/Browser/BrowserFrame";
import { SupersetDashboard } from "@/components/SupersetDashboard";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@ -1119,10 +1120,12 @@ export default function SOE() {
<RefreshCw className="w-4 h-4" /> <RefreshCw className="w-4 h-4" />
Sincronização Sincronização
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="config" className="gap-2" data-testid="tab-config"> {(user as any)?.role === "admin" && (
<UserCog className="w-4 h-4" /> <TabsTrigger value="config" className="gap-2" data-testid="tab-config">
Configuração <UserCog className="w-4 h-4" />
</TabsTrigger> Configuração
</TabsTrigger>
)}
</TabsList> </TabsList>
</div> </div>
@ -1301,6 +1304,14 @@ export default function SOE() {
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">Arcádia Insights</CardTitle>
</CardHeader>
<CardContent className="p-0">
<SupersetDashboard dashboardId="executive-summary" height={480} />
</CardContent>
</Card>
</TabsContent> </TabsContent>
<TabsContent value="persons" className="mt-0 space-y-4"> <TabsContent value="persons" className="mt-0 space-y-4">

View File

@ -35,6 +35,35 @@ services:
volumes: volumes:
- uploads:/app/uploads - uploads:/app/uploads
# ── Superset BI ──────────────────────────────────────────────────────────────
superset:
build:
context: ./docker/superset
dockerfile: Dockerfile
restart: unless-stopped
environment:
SUPERSET_SECRET_KEY: ${SUPERSET_SECRET_KEY:-superset-secret-change-in-dev}
SQLALCHEMY_DATABASE_URI: postgresql://${PGUSER:-arcadia}:${PGPASSWORD:-SuaSenhaSegura}@db:5432/superset
DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD:-SuaSenhaSegura}@db:5432/superset
PYTHONPATH: /app/pythonpath
SUPERSET_ADMIN_USER: ${SUPERSET_ADMIN_USER:-admin}
SUPERSET_ADMIN_USERNAME: ${SUPERSET_ADMIN_USER:-admin}
SUPERSET_ADMIN_PASSWORD: ${SUPERSET_ADMIN_PASSWORD:-arcadia2026}
SUPERSET_ADMIN_EMAIL: ${SUPERSET_ADMIN_EMAIL:-admin@arcadia.app}
ARCADIA_DATABASE_URL: postgresql+psycopg2://${PGUSER:-arcadia}:${PGPASSWORD:-SuaSenhaSegura}@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
ports:
- "8088:8088"
depends_on:
db:
condition: service_healthy
profiles: [bi]
volumes: volumes:
pgdata: pgdata:
uploads: uploads:
superset_home:

View File

@ -52,7 +52,7 @@ export function registerSupersetRoutes(app: Express): void {
last_name: user?.name?.split(" ").slice(1).join(" ") || "User", last_name: user?.name?.split(" ").slice(1).join(" ") || "User",
}, },
resources: [{ type: "dashboard", id: dashboardId }], resources: [{ type: "dashboard", id: dashboardId }],
rls: user?.tenantId ? [{ clause: `tenant_id = ${user.tenantId}`, type: "BASE_ROLE" }] : [], rls: user?.tenantId ? [{ clause: `tenant_id = ${user.tenantId}` }] : [],
}), }),
}); });