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:
parent
264029eafc
commit
c665f6ec40
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue