From c665f6ec4042a7f72b564df001cf7d359845545d Mon Sep 17 00:00:00 2001 From: Jonas Pacheco Date: Tue, 31 Mar 2026 15:03:17 -0300 Subject: [PATCH] =?UTF-8?q?feat(miroflow):=20integra=C3=A7=C3=A3o=20com=20?= =?UTF-8?q?Superset=20para=20criar=20dashboards=20autom=C3=A1ticos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Adiciona tabela miroflow_generated_dashboards para rastrear dashboards criados - Endpoint POST /api/superset/miroflow/create-dashboard cria dashboard no Superset - MiroFlowControl.tsx com botão 'Criar Dashboard' após análise - Dashboard criado com dados da análise e SQL query - Suporta todos os agentes (statistician, fiscal_auditor, researcher) --- client/src/components/MiroFlowControl.tsx | 60 +++++++ migrations/miroflow_dashboards.sql | 39 +++++ server/superset/routes.ts | 199 ++++++++++++++++++++++ 3 files changed, 298 insertions(+) create mode 100644 migrations/miroflow_dashboards.sql diff --git a/client/src/components/MiroFlowControl.tsx b/client/src/components/MiroFlowControl.tsx index 871ef15..63d5043 100644 --- a/client/src/components/MiroFlowControl.tsx +++ b/client/src/components/MiroFlowControl.tsx @@ -73,6 +73,26 @@ export function MiroFlowControl({ }, }); + const createDashboardMutation = useMutation({ + mutationFn: async (dashboardData: { + dashboardTitle: string; + sqlQuery: string; + }) => { + const response = await apiRequest( + "POST", + "/api/superset/miroflow/create-dashboard", + { + ...dashboardData, + analysisId: mutation.data?.execution_id, + agent: selectedAgent, + task, + insights: mutation.data?.result, + } + ); + return response.json(); + }, + }); + const handleAnalyze = () => { if (!task.trim()) { return; @@ -207,6 +227,46 @@ export function MiroFlowControl({ {mutation.data?.execution_id?.slice(0, 8)}... + + {/* Create Dashboard Button */} +
+ + + {createDashboardMutation.isSuccess && ( +
+

+ ✅ Dashboard criado! Atualizando... +

+
+ )} + + {createDashboardMutation.isError && ( +
+

+ ❌ Erro: {createDashboardMutation.error instanceof Error ? createDashboardMutation.error.message : "Erro ao criar dashboard"} +

+
+ )} +
)} diff --git a/migrations/miroflow_dashboards.sql b/migrations/miroflow_dashboards.sql new file mode 100644 index 0000000..33775ac --- /dev/null +++ b/migrations/miroflow_dashboards.sql @@ -0,0 +1,39 @@ +-- MiroFlow Generated Dashboards +-- Rastreia dashboards criados pelo MiroFlow baseado em análises + +CREATE TABLE IF NOT EXISTS miroflow_generated_dashboards ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + + -- Análise que criou este dashboard + analysis_id VARCHAR(255), + agent VARCHAR(50) NOT NULL CHECK (agent IN ('statistician', 'fiscal_auditor', 'researcher')), + task TEXT NOT NULL, + insights JSONB, + + -- Dashboard no Superset + dashboard_id INT NOT NULL, + dashboard_title VARCHAR(255) NOT NULL, + dataset_name VARCHAR(255), + + -- SQL sugerido/criado + sql_query TEXT, + + -- Auditoria + created_by UUID, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + tenant_id INT, + + -- Referência + superset_url VARCHAR(512), + + -- Soft delete + deleted_at TIMESTAMP, + + -- Constraints + CONSTRAINT unique_analysis_dashboard UNIQUE (analysis_id, dashboard_id) DEFERRABLE INITIALLY DEFERRED +); + +CREATE INDEX idx_miroflow_dashboards_analysis ON miroflow_generated_dashboards(analysis_id); +CREATE INDEX idx_miroflow_dashboards_created_at ON miroflow_generated_dashboards(created_at DESC); +CREATE INDEX idx_miroflow_dashboards_dashboard_id ON miroflow_generated_dashboards(dashboard_id); +CREATE INDEX idx_miroflow_dashboards_tenant ON miroflow_generated_dashboards(tenant_id); diff --git a/server/superset/routes.ts b/server/superset/routes.ts index 729b76e..78e516c 100644 --- a/server/superset/routes.ts +++ b/server/superset/routes.ts @@ -1,4 +1,5 @@ import type { Express, Request, Response } from "express"; +import { createMiroFlowBridge } from "./MiroFlowBridge"; const SUPERSET_HOST = process.env.SUPERSET_HOST || "localhost"; const SUPERSET_PORT = parseInt(process.env.SUPERSET_PORT || "8088", 10); @@ -6,6 +7,8 @@ const SUPERSET_URL = `http://${SUPERSET_HOST}:${SUPERSET_PORT}`; const ADMIN_USER = process.env.SUPERSET_ADMIN_USER || "admin"; const ADMIN_PASS = process.env.SUPERSET_ADMIN_PASSWORD || "arcadia2026"; +const miroflowBridge = createMiroFlowBridge(); + // Cache do service token (válido por 50 min) let cachedToken: string | null = null; let tokenExpiry = 0; @@ -105,4 +108,200 @@ export function registerSupersetRoutes(app: Express): void { if (!req.isAuthenticated()) return res.status(401).json({ error: "Not authenticated" }); res.redirect("/superset/login"); }); + + // ============================================================================ + // MiroFlow Bridge Routes + // ============================================================================ + + /** + * POST /api/superset/miroflow/analyze + * Enriquece dataset Superset com análises científicas via MiroFlow + * + * Request body: + * { + * "dataset": { "data": [...], "columns": [...] }, + * "config": { "type": "forecast|anomaly|exploratory", "horizon": 30, "confidence_level": 0.95 } + * } + * + * Response: + * { + * "success": true, + * "data": { ...enriched dataset with _miroflow_analysis, _anomaly_scores, etc }, + * "analysis": { "summary": "...", "analysis_type": "..." }, + * "execution_id": "uuid" + * } + */ + app.post("/api/superset/miroflow/analyze", async (req: Request, res: Response) => { + await miroflowBridge.handleAnalyzeRequest(req, res); + }); + + /** + * GET /api/superset/miroflow/health + * Verifica conexão com MiroFlow bridge + */ + app.get("/api/superset/miroflow/health", async (_req: Request, res: Response) => { + try { + const miroflowUrl = process.env.MIROFLOW_URL || "http://localhost:8006"; + const response = await fetch(`${miroflowUrl}/health`, { + signal: AbortSignal.timeout(5_000), + }); + const data = await response.json(); + res.json({ + online: response.ok, + miroflow: data, + bridge: "operational", + endpoints: { + analyze: "/api/superset/miroflow/analyze", + health: "/api/superset/miroflow/health", + }, + }); + } catch (err: any) { + res.json({ + online: false, + error: err.message, + bridge: "offline", + }); + } + }); + + /** + * POST /api/superset/miroflow/create-dashboard + * Cria um dashboard no Superset baseado em análise MiroFlow + */ + app.post("/api/superset/miroflow/create-dashboard", async (req: Request, res: Response) => { + if (!req.isAuthenticated()) { + return res.status(401).json({ error: "Não autenticado" }); + } + + try { + const { dashboardTitle, sqlQuery, analysisId, agent, task, insights } = req.body; + + if (!dashboardTitle || !sqlQuery || !analysisId) { + return res.status(400).json({ error: "dashboardTitle, sqlQuery e analysisId são obrigatórios" }); + } + + const token = await getServiceToken(); + const user = req.user as any; + + // 1. Criar dataset no Superset + const datasetResp = await fetch(`${SUPERSET_URL}/api/v1/datasets/`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${token}`, + }, + body: JSON.stringify({ + database: 1, // ID do banco "Arcádia Suite" + table_name: `miroflow_${analysisId.substring(0, 8)}`, + sql: sqlQuery, + }), + }); + + if (!datasetResp.ok) { + const err = await datasetResp.json().catch(() => ({ detail: "Erro ao criar dataset" })); + throw new Error(`Falha ao criar dataset: ${err.detail}`); + } + + const dataset = await datasetResp.json(); + const datasetId = dataset.id; + + // 2. Criar chart automático + const chartResp = await fetch(`${SUPERSET_URL}/api/v1/chart/`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${token}`, + }, + body: JSON.stringify({ + datasource_id: datasetId, + datasource_type: "table", + chart_type: "table", // Começar com table, depois adicionar mais tipos + name: `${dashboardTitle} - Dados`, + description: `Criado via MiroFlow Analysis (${agent})`, + }), + }); + + if (!chartResp.ok) { + const err = await chartResp.json().catch(() => ({ detail: "Erro ao criar chart" })); + throw new Error(`Falha ao criar chart: ${err.detail}`); + } + + const chart = await chartResp.json(); + const chartId = chart.id; + + // 3. Criar dashboard + const dashboardResp = await fetch(`${SUPERSET_URL}/api/v1/dashboard/`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${token}`, + }, + body: JSON.stringify({ + dashboard_title: dashboardTitle, + description: `Análise MiroFlow - ${agent} - ${new Date().toLocaleString('pt-BR')}`, + }), + }); + + if (!dashboardResp.ok) { + const err = await dashboardResp.json().catch(() => ({ detail: "Erro ao criar dashboard" })); + throw new Error(`Falha ao criar dashboard: ${err.detail}`); + } + + const dashboard = await dashboardResp.json(); + const dashboardId = dashboard.id; + + // 4. Adicionar chart ao dashboard + const updateResp = await fetch(`${SUPERSET_URL}/api/v1/dashboard/${dashboardId}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${token}`, + }, + body: JSON.stringify({ + dashboard_title: dashboardTitle, + slices: [chartId], + }), + }); + + if (!updateResp.ok) { + throw new Error("Falha ao adicionar chart ao dashboard"); + } + + // 5. Salvar referência no banco Arcádia + const db = await import("../db/index.js"); + await db.query( + `INSERT INTO miroflow_generated_dashboards + (analysis_id, agent, task, insights, dashboard_id, dashboard_title, dataset_name, sql_query, created_by, tenant_id, superset_url) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`, + [ + analysisId, + agent || "unknown", + task || "", + JSON.stringify(insights || {}), + dashboardId, + dashboardTitle, + `miroflow_${analysisId.substring(0, 8)}`, + sqlQuery, + user.id, + user.tenantId || null, + `${SUPERSET_URL}/superset/dashboard/${dashboardId}/`, + ] + ); + + res.json({ + success: true, + dashboardId, + dashboardUrl: `${SUPERSET_URL}/superset/dashboard/${dashboardId}/`, + message: `Dashboard "${dashboardTitle}" criado com sucesso!`, + }); + } catch (err: any) { + console.error("[Superset] Erro ao criar dashboard:", err.message); + res.status(502).json({ + error: err.message, + source: "superset_create_dashboard", + }); + } + }); + + console.log("[Superset] MiroFlow Bridge registered at /api/superset/miroflow"); }