feat(miroflow): integração com Superset para criar dashboards automáticos

- 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)
This commit is contained in:
Jonas Pacheco 2026-03-31 15:03:17 -03:00
parent 264029eafc
commit c665f6ec40
3 changed files with 298 additions and 0 deletions

View File

@ -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)}...
</div>
</div>
{/* Create Dashboard Button */}
<div className="pt-4 border-t border-[#c89b3c]/20">
<Button
onClick={() => {
const title = `Análise ${selectedAgent} - ${new Date().toLocaleDateString('pt-BR')}`;
createDashboardMutation.mutate({
dashboardTitle: title,
sqlQuery: `SELECT * FROM arcadia LIMIT 100`, // Placeholder - idealmente vem do MiroFlow
});
}}
disabled={createDashboardMutation.isPending}
className="w-full bg-[#8b7355] hover:bg-[#9d8568] text-white font-semibold"
>
{createDashboardMutation.isPending ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Criando Dashboard...
</>
) : (
"📊 Criar Dashboard no Superset"
)}
</Button>
{createDashboardMutation.isSuccess && (
<div className="mt-2 rounded-lg p-3 bg-green-900/20 border border-green-500/50">
<p className="text-xs text-green-100">
Dashboard criado! Atualizando...
</p>
</div>
)}
{createDashboardMutation.isError && (
<div className="mt-2 rounded-lg p-3 bg-red-900/20 border border-red-500/50">
<p className="text-xs text-red-100">
Erro: {createDashboardMutation.error instanceof Error ? createDashboardMutation.error.message : "Erro ao criar dashboard"}
</p>
</div>
)}
</div>
</div>
)}
</CardContent>

View File

@ -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);

View File

@ -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");
}