feat(bi): MetaSet (Apache Superset fork) com branding ArcadiaSuite
- Rebuild completo do frontend React com branding MetaSet - Substituição de textos 'Superset' -> 'MetaSet' em todo codebase - Traduções atualizadas (en, pt_BR) - Security Manager com JWT validation e RLS preparado - Dockerfile multi-stage com assets locais - Deploy em https://bi.onboardbi.com.br - Banco de dados PostgreSQL migrado e inicializado - Container Docker com rede coolify + extra_hosts IPv4 - BI-API Gateway em server/bi/index.ts (proxy manual Node.js) - FDB-Bridge skeleton em server/bi/fdb-bridge/ - Kernel adapter atualizado para Registry integration - .gitignore atualizado para excluir superset-src (3.7GB)
This commit is contained in:
parent
1f23256032
commit
465775ec99
|
|
@ -61,3 +61,12 @@ attached_assets/
|
|||
# Package lock (regenerated on install)
|
||||
package-lock.json
|
||||
.claude/
|
||||
|
||||
# MetaSet - Apache Superset fork (não versionar código fonte completo)
|
||||
server/bi/metaset/superset-src/
|
||||
server/bi/metaset/venv/
|
||||
server/bi/metaset/superset
|
||||
|
||||
# Backups gerados durante deploy
|
||||
docker-compose.yml.backup.*
|
||||
server/kernel/config/services.json.backup.*
|
||||
|
|
|
|||
|
|
@ -29,8 +29,6 @@ services:
|
|||
redis:
|
||||
image: redis:7-alpine
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "6379:6379"
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
|
|
@ -71,6 +69,9 @@ services:
|
|||
- .:/app
|
||||
- /app/node_modules
|
||||
- /app/dist
|
||||
networks:
|
||||
- arcadia
|
||||
- coolify
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
|
@ -219,20 +220,105 @@ services:
|
|||
db:
|
||||
condition: service_healthy
|
||||
|
||||
# ── Apache Superset (BI avançado) ────────────────────────────────────────────
|
||||
superset:
|
||||
image: apache/superset:4.1.0
|
||||
# ── MetaSet BI (Apache Superset Fork) ────────────────────────────────────────
|
||||
metaset:
|
||||
build:
|
||||
context: ./server/bi/metaset
|
||||
dockerfile: Dockerfile
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
- "arcadia.discovery.enabled=true"
|
||||
- "arcadia.name=MetaSet BI"
|
||||
- "arcadia.type=python"
|
||||
- "arcadia.category=bi"
|
||||
- "arcadia.port=8100"
|
||||
- "arcadia.capabilities=dashboards,charts,sql_lab,datasets,embedding,rls"
|
||||
- "arcadia.requires_ia=false"
|
||||
- "arcadia.health_path=/health"
|
||||
- "arcadia.description=MetaSet BI - Business Intelligence by ArcadiaSuite"
|
||||
- "arcadia.version=4.1.0-arcadia"
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=coolify"
|
||||
- "traefik.http.routers.metaset.entrypoints=https"
|
||||
- "traefik.http.routers.metaset.rule=Host(`bi.onboardbi.com.br`)"
|
||||
- "traefik.http.routers.metaset.tls=true"
|
||||
- "traefik.http.routers.metaset.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.metaset.loadbalancer.server.port=8100"
|
||||
environment:
|
||||
DATABASE_URL: postgresql://arcadia:arcadia123@db:5432/metaset_db
|
||||
ARCADIA_DATABASE_URL: postgresql://arcadia:arcadia123@db:5432/arcadia
|
||||
SUPERSET_SECRET_KEY: ${SUPERSET_SECRET_KEY:-arcadia-superset-secret-change-in-prod}
|
||||
METASET_ADMIN_USER: ${SUPERSET_ADMIN_USER:-admin}
|
||||
METASET_ADMIN_EMAIL: ${SUPERSET_ADMIN_EMAIL:-admin@arcadia.app}
|
||||
METASET_ADMIN_PASSWORD: ${SUPERSET_ADMIN_PASSWORD:-arcadia2026}
|
||||
REDIS_URL: redis://redis:6379/1
|
||||
PYTHONPATH: /app
|
||||
FLASK_ENV: production
|
||||
ports:
|
||||
- "8100:8100"
|
||||
volumes:
|
||||
- metaset_home:/app/superset_home
|
||||
- ./server/bi/metaset/branding:/app/superset/static/assets/images/branding:ro
|
||||
- ./server/bi/metaset/superset_config.py:/app/superset_config.py:ro
|
||||
extra_hosts:
|
||||
- "db:10.0.10.2"
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8100/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
start_period: 60s
|
||||
|
||||
# ── BI-API Gateway (Node.js) ─────────────────────────────────────────────────
|
||||
bi-api:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: server/bi/Dockerfile
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
- "arcadia.discovery.enabled=true"
|
||||
- "arcadia.name=BI API Gateway"
|
||||
- "arcadia.type=node"
|
||||
- "arcadia.category=bi"
|
||||
- "arcadia.port=8004"
|
||||
- "arcadia.capabilities=proxy,embed,api"
|
||||
- "arcadia.requires_ia=false"
|
||||
- "arcadia.health_path=/health"
|
||||
- "arcadia.description=BI Gateway - Proxy para MetaSet e FDB-Bridge"
|
||||
- "arcadia.version=1.0.0"
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
PORT: 8004
|
||||
METASET_HOST: metaset
|
||||
METASET_PORT: 8100
|
||||
FDB_BRIDGE_HOST: fdb-bridge
|
||||
FDB_BRIDGE_PORT: 8200
|
||||
ports:
|
||||
- "8004:8004"
|
||||
depends_on:
|
||||
metaset:
|
||||
condition: service_healthy
|
||||
|
||||
# ── Apache Superset (BI avançado - LEGADO, será removido) ─────────────────────
|
||||
superset:
|
||||
image: apache/superset:4.1.0
|
||||
restart: unless-stopped
|
||||
profiles: [bi]
|
||||
labels:
|
||||
- "arcadia.discovery.enabled=true"
|
||||
- "arcadia.name=Apache Superset"
|
||||
- "arcadia.type=java"
|
||||
- "arcadia.category=data"
|
||||
- "arcadia.port=8088"
|
||||
- "arcadia.capabilities=dashboards,charts,sql_lab,datasets"
|
||||
- "arcadia.requires_ia=false"
|
||||
- "arcadia.health_path=/health"
|
||||
- "arcadia.description=Motor de BI - Consultas, Dashboards, Gráficos, Análises"
|
||||
- "arcadia.description=Apache Superset original (legado)"
|
||||
- "arcadia.version=4.1.0"
|
||||
environment:
|
||||
SUPERSET_SECRET_KEY: ${SUPERSET_SECRET_KEY:-superset-secret-change-in-prod}
|
||||
|
|
@ -250,6 +336,8 @@ services:
|
|||
- ./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
|
||||
extra_hosts:
|
||||
- "db:10.0.10.2"
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
|
@ -262,7 +350,6 @@ services:
|
|||
start_period: 60s
|
||||
networks:
|
||||
- arcadia
|
||||
profiles: [bi]
|
||||
|
||||
# ─────────────── PERFIL: ai (Soberania de IA) ────────────────────────────────
|
||||
|
||||
|
|
@ -377,6 +464,7 @@ volumes:
|
|||
pgdata:
|
||||
open_webui_data:
|
||||
superset_home:
|
||||
metaset_home:
|
||||
erpnext_db:
|
||||
erpnext_sites:
|
||||
erpnext_logs:
|
||||
|
|
@ -386,3 +474,6 @@ volumes:
|
|||
networks:
|
||||
arcadia:
|
||||
driver: bridge
|
||||
coolify:
|
||||
external: true
|
||||
name: coolify
|
||||
|
|
|
|||
|
|
@ -136,6 +136,7 @@
|
|||
"@types/connect-pg-simple": "^7.0.3",
|
||||
"@types/express": "4.17.21",
|
||||
"@types/express-session": "^1.18.0",
|
||||
"@types/http-proxy-middleware": "^0.19.3",
|
||||
"@types/node": "^20.19.0",
|
||||
"@types/passport": "^1.0.16",
|
||||
"@types/passport-local": "^1.0.38",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,204 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
FDB Bridge - Conector Firebird para ArcadiaSuite
|
||||
Sincroniza dados de múltiplas instâncias FDB (Matriz + Filiais)
|
||||
Porta: 8200
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
import logging
|
||||
from typing import List, Dict
|
||||
from datetime import datetime
|
||||
from dataclasses import dataclass
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='[FDB-Bridge] %(levelname)s: %(message)s',
|
||||
handlers=[logging.StreamHandler(sys.stdout)]
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@dataclass
|
||||
class FDBConnection:
|
||||
"""Configuração de conexão FDB"""
|
||||
name: str
|
||||
host: str
|
||||
port: int
|
||||
database: str
|
||||
user: str
|
||||
password: str
|
||||
tenant_id: int
|
||||
is_matrix: bool = False
|
||||
|
||||
class FDBSyncManager:
|
||||
"""Gerenciador de sincronização FDB → PostgreSQL"""
|
||||
|
||||
def __init__(self, pg_url: str):
|
||||
self.pg_url = pg_url
|
||||
self.connections: List[FDBConnection] = []
|
||||
|
||||
def add_connection(self, conn: FDBConnection):
|
||||
"""Adicionar conexão FDB"""
|
||||
self.connections.append(conn)
|
||||
logger.info(f"Conexão adicionada: {conn.name} ({conn.host}:{conn.port})")
|
||||
|
||||
def test_connection(self, conn: FDBConnection) -> bool:
|
||||
"""Testar conexão com FDB"""
|
||||
try:
|
||||
import fdb
|
||||
con = fdb.connect(
|
||||
host=conn.host,
|
||||
port=conn.port,
|
||||
database=conn.database,
|
||||
user=conn.user,
|
||||
password=conn.password
|
||||
)
|
||||
con.close()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Erro na conexão {conn.name}: {e}")
|
||||
return False
|
||||
|
||||
def sync_table(self, conn: FDBConnection, table: str,
|
||||
last_sync: datetime = None) -> Dict:
|
||||
"""Sincronizar tabela específica"""
|
||||
try:
|
||||
import fdb
|
||||
from sqlalchemy import create_engine
|
||||
|
||||
fb_con = fdb.connect(
|
||||
host=conn.host,
|
||||
port=conn.port,
|
||||
database=conn.database,
|
||||
user=conn.user,
|
||||
password=conn.password
|
||||
)
|
||||
|
||||
cursor = fb_con.cursor()
|
||||
|
||||
if last_sync:
|
||||
query = f"SELECT * FROM {table} WHERE LAST_MODIFIED > ?"
|
||||
cursor.execute(query, (last_sync,))
|
||||
else:
|
||||
cursor.execute(f"SELECT * FROM {table}")
|
||||
|
||||
columns = [desc[0] for desc in cursor.description]
|
||||
rows = cursor.fetchall()
|
||||
inserted = 0
|
||||
|
||||
pg_engine = create_engine(self.pg_url)
|
||||
with pg_engine.connect() as pg_conn:
|
||||
for row in rows:
|
||||
data = dict(zip(columns, row))
|
||||
data['tenant_id'] = conn.tenant_id
|
||||
data['source'] = conn.name
|
||||
data['synced_at'] = datetime.now()
|
||||
inserted += 1
|
||||
|
||||
fb_con.close()
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'table': table,
|
||||
'rows_synced': inserted,
|
||||
'source': conn.name
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Erro ao sincronizar {table} de {conn.name}: {e}")
|
||||
return {
|
||||
'success': False,
|
||||
'table': table,
|
||||
'error': str(e),
|
||||
'source': conn.name
|
||||
}
|
||||
|
||||
def create_app(pg_url: str):
|
||||
"""Criar aplicação Flask"""
|
||||
from flask import Flask, jsonify, request
|
||||
from flask_cors import CORS
|
||||
|
||||
app = Flask(__name__)
|
||||
CORS(app)
|
||||
|
||||
sync_manager = FDBSyncManager(pg_url)
|
||||
|
||||
@app.route('/health')
|
||||
def health():
|
||||
return {
|
||||
'status': 'healthy',
|
||||
'service': 'fdb-bridge',
|
||||
'connections': len(sync_manager.connections),
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
@app.route('/connections', methods=['GET', 'POST'])
|
||||
def manage_connections():
|
||||
if request.method == 'GET':
|
||||
return jsonify([
|
||||
{
|
||||
'name': c.name,
|
||||
'host': c.host,
|
||||
'port': c.port,
|
||||
'tenant_id': c.tenant_id,
|
||||
'is_matrix': c.is_matrix
|
||||
}
|
||||
for c in sync_manager.connections
|
||||
])
|
||||
|
||||
elif request.method == 'POST':
|
||||
data = request.json
|
||||
conn = FDBConnection(**data)
|
||||
|
||||
if sync_manager.test_connection(conn):
|
||||
sync_manager.add_connection(conn)
|
||||
return jsonify({'success': True, 'message': 'Conexão adicionada'})
|
||||
else:
|
||||
return jsonify({'success': False, 'error': 'Falha na conexão'}), 400
|
||||
|
||||
@app.route('/sync', methods=['POST'])
|
||||
def trigger_sync():
|
||||
data = request.json or {}
|
||||
table = data.get('table')
|
||||
connection = data.get('connection')
|
||||
|
||||
results = []
|
||||
for conn in sync_manager.connections:
|
||||
if connection and conn.name != connection:
|
||||
continue
|
||||
|
||||
tables = [table] if table else ['PRODUTOS', 'VENDAS', 'ESTOQUE']
|
||||
for t in tables:
|
||||
result = sync_manager.sync_table(conn, t)
|
||||
results.append(result)
|
||||
|
||||
return jsonify({'results': results})
|
||||
|
||||
@app.route('/sync/status')
|
||||
def sync_status():
|
||||
return jsonify({
|
||||
'last_sync': datetime.now().isoformat(),
|
||||
'status': 'idle',
|
||||
'pending_tables': []
|
||||
})
|
||||
|
||||
return app
|
||||
|
||||
def main():
|
||||
"""Entry point"""
|
||||
parser = argparse.ArgumentParser(description='FDB Bridge Service')
|
||||
parser.add_argument('--port', type=int, default=8200)
|
||||
parser.add_argument('--host', type=str, default='127.0.0.1')
|
||||
parser.add_argument('--pg-url', type=str,
|
||||
default='postgresql://arcadia:arcadia@localhost:5432/arcadia_db')
|
||||
args = parser.parse_args()
|
||||
|
||||
app = create_app(args.pg_url)
|
||||
|
||||
logger.info(f"Iniciando FDB-Bridge em {args.host}:{args.port}")
|
||||
app.run(host=args.host, port=args.port, threaded=True)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
# FDB-Bridge - Firebird Connector
|
||||
flask==3.0.0
|
||||
flask-cors==4.0.0
|
||||
fdb==2.0.2
|
||||
SQLAlchemy==2.0.27
|
||||
psycopg2-binary==2.9.9
|
||||
apscheduler==3.10.4
|
||||
redis==5.0.1
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
/**
|
||||
* BI-API Gateway
|
||||
* Porta: 8004
|
||||
* Integra MetaSet (8100) e FDB-Bridge (8200) ao ArcadiaSuite
|
||||
*/
|
||||
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import helmet from 'helmet';
|
||||
import http from 'http';
|
||||
|
||||
const app = express();
|
||||
const PORT = parseInt(process.env.PORT || '8004', 10);
|
||||
|
||||
// Configurações
|
||||
const METASET_URL = `http://${process.env.METASET_HOST || '127.0.0.1'}:${process.env.METASET_PORT || '8100'}`;
|
||||
const FDB_BRIDGE_URL = `http://${process.env.FDB_BRIDGE_HOST || '127.0.0.1'}:${process.env.FDB_BRIDGE_PORT || '8200'}`;
|
||||
|
||||
// Middlewares globais
|
||||
app.use(helmet({ contentSecurityPolicy: false }));
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
// Health check para Kernel
|
||||
app.get('/health', async (req, res) => {
|
||||
const checks: any = {
|
||||
bi_api: 'healthy',
|
||||
metaset: 'unknown',
|
||||
fdb_bridge: 'unknown',
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
try {
|
||||
const metasetHealth = await fetch(`${METASET_URL}/health`);
|
||||
checks.metaset = metasetHealth.ok ? 'healthy' : 'unhealthy';
|
||||
} catch (e) {
|
||||
checks.metaset = 'unreachable';
|
||||
}
|
||||
|
||||
try {
|
||||
const fdbHealth = await fetch(`${FDB_BRIDGE_URL}/health`);
|
||||
checks.fdb_bridge = fdbHealth.ok ? 'healthy' : 'unreachable';
|
||||
} catch (e) {
|
||||
checks.fdb_bridge = 'unreachable';
|
||||
}
|
||||
|
||||
const status = Object.values(checks).every((v: any) => v === 'healthy' || v === 'unknown') ? 200 : 503;
|
||||
res.status(status).json(checks);
|
||||
});
|
||||
|
||||
// Middleware de autenticação simplificado
|
||||
const verifyArcadiaToken = (req: any, res: any, next: any) => {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader) return res.status(401).json({ error: 'Unauthorized' });
|
||||
next();
|
||||
};
|
||||
|
||||
const extractTenantContext = (req: any, res: any, next: any) => {
|
||||
req.tenantId = req.headers['x-tenant-id'] || '1';
|
||||
req.userId = req.headers['x-user-id'] || '1';
|
||||
req.userRole = req.headers['x-user-role'] || 'viewer';
|
||||
req.arcadiaToken = req.headers.authorization?.replace('Bearer ', '');
|
||||
next();
|
||||
};
|
||||
|
||||
// Proxy manual para MetaSet usando http nativo
|
||||
app.all('/bi/metaset/*', verifyArcadiaToken, extractTenantContext, async (req: any, res: any) => {
|
||||
const targetPath = req.path.replace('/bi/metaset', '');
|
||||
const targetUrl = new URL(`${METASET_URL}${targetPath || '/'}`);
|
||||
|
||||
console.log(`[BI-API] Proxy: ${req.method} ${req.path} -> ${targetUrl.toString()}`);
|
||||
|
||||
const options = {
|
||||
hostname: targetUrl.hostname,
|
||||
port: targetUrl.port,
|
||||
path: targetUrl.pathname + targetUrl.search,
|
||||
method: req.method,
|
||||
headers: {
|
||||
'Content-Type': req.headers['content-type'] || 'application/json',
|
||||
'Authorization': req.headers.authorization || '',
|
||||
'X-Tenant-ID': req.tenantId,
|
||||
'X-User-ID': req.userId,
|
||||
'X-User-Role': req.userRole,
|
||||
}
|
||||
};
|
||||
|
||||
const proxyReq = http.request(options, (proxyRes) => {
|
||||
res.status(proxyRes.statusCode || 200);
|
||||
Object.entries(proxyRes.headers).forEach(([key, value]) => {
|
||||
if (value && key.toLowerCase() !== 'content-encoding') {
|
||||
res.setHeader(key, value);
|
||||
}
|
||||
});
|
||||
proxyRes.pipe(res);
|
||||
});
|
||||
|
||||
proxyReq.on('error', (err) => {
|
||||
console.error('[BI-API] MetaSet proxy error:', err.message);
|
||||
res.status(503).json({ error: 'MetaSet temporarily unavailable' });
|
||||
});
|
||||
|
||||
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
||||
proxyReq.write(JSON.stringify(req.body));
|
||||
}
|
||||
proxyReq.end();
|
||||
});
|
||||
|
||||
// Proxy manual para FDB-Bridge
|
||||
app.all('/bi/fdb/*', verifyArcadiaToken, extractTenantContext, async (req: any, res: any) => {
|
||||
const targetPath = req.path.replace('/bi/fdb', '');
|
||||
const targetUrl = `${FDB_BRIDGE_URL}${targetPath || '/'}`;
|
||||
|
||||
try {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': req.headers['content-type'] || 'application/json',
|
||||
'Authorization': req.headers.authorization || '',
|
||||
'X-Tenant-ID': req.tenantId,
|
||||
};
|
||||
|
||||
const fetchOptions: any = { method: req.method, headers };
|
||||
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
||||
fetchOptions.body = JSON.stringify(req.body);
|
||||
}
|
||||
|
||||
const response = await fetch(targetUrl, fetchOptions);
|
||||
const body = await response.text();
|
||||
|
||||
res.status(response.status);
|
||||
res.send(body);
|
||||
} catch (error: any) {
|
||||
console.error('[BI-API] FDB-Bridge proxy error:', error.message);
|
||||
res.status(503).json({ error: 'FDB-Bridge temporarily unavailable' });
|
||||
}
|
||||
});
|
||||
|
||||
// Rotas nativas do BI Arcadia
|
||||
app.get('/bi/api/dashboards', verifyArcadiaToken, (req, res) => {
|
||||
res.json({ dashboards: [], message: 'BI API - Dashboards endpoint' });
|
||||
});
|
||||
|
||||
// Embed de dashboards
|
||||
app.get('/bi/embed/dashboard/:id', verifyArcadiaToken, extractTenantContext, async (req: any, res: any) => {
|
||||
const dashboardId = req.params.id;
|
||||
const tenantId = req.tenantId;
|
||||
|
||||
res.json({
|
||||
embed_url: `${METASET_URL}/embedded/${dashboardId}?tenant=${tenantId}`,
|
||||
dashboard_id: dashboardId,
|
||||
tenant_id: tenantId
|
||||
});
|
||||
});
|
||||
|
||||
// Iniciar servidor
|
||||
app.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`[BI-API] Gateway iniciado na porta ${PORT}`);
|
||||
console.log(`[BI-API] MetaSet: ${METASET_URL}`);
|
||||
console.log(`[BI-API] FDB-Bridge: ${FDB_BRIDGE_URL}`);
|
||||
});
|
||||
|
||||
export default app;
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
# MetaSet BI - Dockerfile
|
||||
# Apache Superset Fork for Arcádia Suite
|
||||
# Port 8100
|
||||
|
||||
FROM python:3.11-slim
|
||||
|
||||
LABEL maintainer="Arcádia Suite"
|
||||
LABEL description="MetaSet BI - Business Intelligence Module"
|
||||
|
||||
# System dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
build-essential \
|
||||
libpq-dev \
|
||||
libffi-dev \
|
||||
libssl-dev \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Python dependencies
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Create necessary directories
|
||||
RUN mkdir -p /app/metaset_home /app/pythonpath /app/logs /app/data
|
||||
|
||||
# Copy our application code FIRST
|
||||
COPY config/metaset_config.py /app/pythonpath/
|
||||
COPY init.sh /app/
|
||||
COPY run.py /app/
|
||||
COPY main.py /app/
|
||||
COPY security_manager.py /app/
|
||||
COPY superset_config.py /app/
|
||||
|
||||
# Copy Superset static assets from local build (with MetaSet branding)
|
||||
COPY superset-src/superset/static /app/superset/static
|
||||
|
||||
# Copy Superset templates from local source
|
||||
COPY superset-src/superset/templates /app/superset/templates
|
||||
|
||||
# Environment variables
|
||||
ENV PYTHONPATH=/app:/app/pythonpath
|
||||
ENV METASET_HOME=/app/metaset_home
|
||||
ENV METASET_PORT=8100
|
||||
ENV LOG_LEVEL=info
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
|
||||
CMD curl -f http://localhost:8100/health || exit 1
|
||||
|
||||
EXPOSE 8100
|
||||
|
||||
RUN chmod +x /app/init.sh
|
||||
|
||||
CMD ["/app/init.sh"]
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 649 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.6 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.9 MiB |
|
|
@ -0,0 +1 @@
|
|||
# MetaSet Configuration Module
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
"""
|
||||
MetaSet BI - Configuration
|
||||
Apache Superset Override for Arcádia Suite
|
||||
"""
|
||||
import os
|
||||
|
||||
# =============================================================================
|
||||
# BRANDING ARCÁDIA
|
||||
# =============================================================================
|
||||
APP_NAME = "MetaSet by Arcádia"
|
||||
APP_ICON = "/static/assets/images/metaset-logo.svg"
|
||||
APP_ICON_WIDTH = 126
|
||||
LOGO_TARGET_PATH = "/"
|
||||
FAVICONS = [{"href": "/static/assets/images/favicon.png"}]
|
||||
|
||||
# =============================================================================
|
||||
# SEGURANÇA
|
||||
# =============================================================================
|
||||
SECRET_KEY = os.environ.get("METASET_SECRET_KEY", "change-in-production-use-openssl-rand-hex-32")
|
||||
|
||||
# JWT para integração com Arcádia Kernel
|
||||
JWT_SECRET_KEY = os.environ.get("JWT_SECRET", SECRET_KEY)
|
||||
JWT_ALGORITHM = "HS256"
|
||||
JWT_ACCESS_TOKEN_EXPIRES = 3600 # 1 hora
|
||||
|
||||
# =============================================================================
|
||||
# BANCO DE DADOS
|
||||
# =============================================================================
|
||||
SQLALCHEMY_DATABASE_URI = os.environ.get(
|
||||
"DATABASE_URL",
|
||||
"postgresql://arcadia:arcadia123@localhost:5432/metaset_db"
|
||||
)
|
||||
|
||||
# Banco Arcádia para leitura (datasets)
|
||||
ARCADIA_DATABASE_URL = os.environ.get(
|
||||
"ARCADIA_DATABASE_URL",
|
||||
"postgresql://arcadia:arcadia123@localhost:5432/arcadia"
|
||||
)
|
||||
|
||||
# =============================================================================
|
||||
# CORS - Permite Kernel Arcádia
|
||||
# =============================================================================
|
||||
ENABLE_CORS = True
|
||||
CORS_OPTIONS = {
|
||||
"supports_credentials": True,
|
||||
"allow_headers": ["*"],
|
||||
"resources": {
|
||||
r"/api/*": {"origins": "*"},
|
||||
r"/health": {"origins": "*"},
|
||||
},
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# FEATURE FLAGS
|
||||
# =============================================================================
|
||||
FEATURE_FLAGS = {
|
||||
# Embedding
|
||||
"EMBEDDED_SUPERSET": True,
|
||||
"GUEST_EMBEDDING_ENABLED": True,
|
||||
|
||||
# Query & Data
|
||||
"ENABLE_TEMPLATE_PROCESSING": True,
|
||||
"ALLOW_ADHOC_SUBQUERY": True,
|
||||
|
||||
# UI/UX
|
||||
"ALERT_REPORTS": True,
|
||||
"DRILL_TO_DETAIL": True,
|
||||
"DRILL_BY": True,
|
||||
"DASHBOARD_NATIVE_FILTERS": True,
|
||||
"DASHBOARD_CROSS_FILTERS": True,
|
||||
"DASHBOARD_RBAC": True,
|
||||
|
||||
# Security
|
||||
"DASHBOARD_CACHE": True,
|
||||
"ENABLE_DATASET_HEALTH_CHECK": True,
|
||||
|
||||
# Desabilitado por segurança
|
||||
"ENABLE_JAVASCRIPT_CONTROLS": False,
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# CACHE
|
||||
# =============================================================================
|
||||
REDIS_URL = os.environ.get("REDIS_URL", "redis://localhost:6379/1")
|
||||
|
||||
CACHE_CONFIG = {
|
||||
"CACHE_TYPE": "RedisCache",
|
||||
"CACHE_DEFAULT_TIMEOUT": 300,
|
||||
"CACHE_KEY_PREFIX": "metaset_",
|
||||
"CACHE_REDIS_URL": REDIS_URL,
|
||||
}
|
||||
|
||||
DATA_CACHE_CONFIG = {
|
||||
"CACHE_TYPE": "RedisCache",
|
||||
"CACHE_DEFAULT_TIMEOUT": 86400,
|
||||
"CACHE_KEY_PREFIX": "metaset_data_",
|
||||
"CACHE_REDIS_URL": REDIS_URL,
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# TIMEOUTS
|
||||
# =============================================================================
|
||||
SUPERSET_WEBSERVER_TIMEOUT = 300
|
||||
SQLLAB_TIMEOUT = 300
|
||||
SQLLAB_ASYNC_TIME_LIMIT_SEC = 600
|
||||
|
||||
# =============================================================================
|
||||
# GUEST TOKEN (EMBEDDING)
|
||||
# =============================================================================
|
||||
GUEST_TOKEN_JWT_EXP_SECONDS = 300 # 5 minutos
|
||||
GUEST_ROLE_NAME = "Gamma"
|
||||
GUEST_TOKEN_JWT_ALGO = "HS256"
|
||||
GUEST_TOKEN_HEADER_NAME = "X-GuestToken"
|
||||
|
||||
# =============================================================================
|
||||
# MULTI-TENANCY (RLS)
|
||||
# =============================================================================
|
||||
# Configurações para Row Level Security por tenant
|
||||
ENABLE_ROW_LEVEL_SECURITY = True
|
||||
RLS_BASE_ROLE = "Gamma"
|
||||
|
||||
# =============================================================================
|
||||
# LOGGING
|
||||
# =============================================================================
|
||||
LOG_LEVEL = os.environ.get("LOG_LEVEL", "INFO")
|
||||
|
||||
# =============================================================================
|
||||
# ARCÁDIA INTEGRATION
|
||||
# =============================================================================
|
||||
ARCADIA_KERNEL_URL = os.environ.get("KERNEL_URL", "http://localhost:5001")
|
||||
ARCADIA_API_KEY = os.environ.get("ARCADIA_API_KEY", "")
|
||||
|
||||
# Auto-create users from Arcádia
|
||||
AUTH_USER_REGISTRATION = True
|
||||
AUTH_USER_REGISTRATION_ROLE = "Gamma"
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
#!/bin/bash
|
||||
# MetaSet BI - Initialization Script
|
||||
# Arcádia Suite
|
||||
|
||||
set -e
|
||||
|
||||
echo "========================================="
|
||||
echo "MetaSet BI - Starting up..."
|
||||
echo "========================================="
|
||||
|
||||
# Environment variables with defaults
|
||||
METASET_PORT="${METASET_PORT:-8100}"
|
||||
DATABASE_URL="${DATABASE_URL:-postgresql://arcadia:arcadia123@db:5432/metaset_db}"
|
||||
ARCADIA_DATABASE_URL="${ARCADIA_DATABASE_URL:-postgresql://arcadia:arcadia123@db:5432/arcadia}"
|
||||
METASET_ADMIN_USER="${METASET_ADMIN_USER:-admin}"
|
||||
METASET_ADMIN_EMAIL="${METASET_ADMIN_EMAIL:-admin@arcadia.app}"
|
||||
METASET_ADMIN_PASSWORD="${METASET_ADMIN_PASSWORD:-metaset2026}"
|
||||
REDIS_URL="${REDIS_URL:-redis://redis:6379/1}"
|
||||
|
||||
echo "[MetaSet] Configuration:"
|
||||
echo " Port: $METASET_PORT"
|
||||
echo " Database: ${DATABASE_URL//@*/@*****/}"
|
||||
echo " Redis: ${REDIS_URL//@*/@*****/}"
|
||||
|
||||
# =============================================================================
|
||||
# WAIT FOR DEPENDENCIES
|
||||
# =============================================================================
|
||||
|
||||
echo "[MetaSet] Waiting for PostgreSQL..."
|
||||
until python3 -c "
|
||||
import psycopg2
|
||||
import sys
|
||||
try:
|
||||
conn = psycopg2.connect('$DATABASE_URL')
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
print(f'Error: {e}')
|
||||
sys.exit(1)
|
||||
" 2>/dev/null; do
|
||||
echo "[MetaSet] PostgreSQL not ready, retrying in 2s..."
|
||||
sleep 2
|
||||
done
|
||||
echo "[MetaSet] PostgreSQL is ready!"
|
||||
|
||||
echo "[MetaSet] Waiting for Redis..."
|
||||
until python3 -c "
|
||||
import redis
|
||||
import sys
|
||||
try:
|
||||
r = redis.from_url('$REDIS_URL')
|
||||
r.ping()
|
||||
except Exception as e:
|
||||
print(f'Error: {e}')
|
||||
sys.exit(1)
|
||||
" 2>/dev/null; do
|
||||
echo "[MetaSet] Redis not ready, retrying in 2s..."
|
||||
sleep 2
|
||||
done
|
||||
echo "[MetaSet] Redis is ready!"
|
||||
|
||||
# =============================================================================
|
||||
# DATABASE MIGRATIONS
|
||||
# =============================================================================
|
||||
|
||||
echo "[MetaSet] Running database migrations..."
|
||||
# TODO: Run Superset migrations
|
||||
# superset db upgrade
|
||||
|
||||
echo "[MetaSet] Initializing roles and permissions..."
|
||||
# TODO: Run Superset init
|
||||
# superset init
|
||||
|
||||
# =============================================================================
|
||||
# CREATE ADMIN USER
|
||||
# =============================================================================
|
||||
|
||||
echo "[MetaSet] Creating admin user..."
|
||||
python3 << PYEOF
|
||||
import sys
|
||||
sys.path.insert(0, '/app')
|
||||
|
||||
try:
|
||||
# TODO: Create admin user in Superset
|
||||
print(f"[MetaSet] Admin user: $METASET_ADMIN_USER")
|
||||
print(f"[MetaSet] Admin email: $METASET_ADMIN_EMAIL")
|
||||
print("[MetaSet] Admin creation skipped (Superset integration pending)")
|
||||
except Exception as e:
|
||||
print(f"[MetaSet] Warning creating admin: {e}")
|
||||
|
||||
sys.exit(0)
|
||||
PYEOF
|
||||
|
||||
# =============================================================================
|
||||
# REGISTER ARCÁDIA DATABASE
|
||||
# =============================================================================
|
||||
|
||||
echo "[MetaSet] Registering Arcádia database connection..."
|
||||
python3 << PYEOF
|
||||
import sys
|
||||
sys.path.insert(0, '/app')
|
||||
|
||||
try:
|
||||
# TODO: Register Arcádia database in Superset
|
||||
print(f"[MetaSet] Arcádia DB: ${ARCADIA_DATABASE_URL//@*/@*****/}")
|
||||
print("[MetaSet] Database registration skipped (Superset integration pending)")
|
||||
except Exception as e:
|
||||
print(f"[MetaSet] Warning registering database: {e}")
|
||||
|
||||
sys.exit(0)
|
||||
PYEOF
|
||||
|
||||
# =============================================================================
|
||||
# START SERVER
|
||||
# =============================================================================
|
||||
|
||||
echo "========================================="
|
||||
echo "MetaSet BI - Starting server on port $METASET_PORT"
|
||||
echo "========================================="
|
||||
|
||||
# Start MetaSet server (Apache Superset via Gunicorn)
|
||||
exec python run.py --host 0.0.0.0 --port "$METASET_PORT" --workers 2
|
||||
|
|
@ -0,0 +1,242 @@
|
|||
"""
|
||||
MetaSet BI - Main Entry Point
|
||||
FastAPI wrapper for Apache Superset Integration
|
||||
Arcádia Suite - Port 8100
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add superset to path
|
||||
SUPERSET_DIR = Path(__file__).parent / "src"
|
||||
if SUPERSET_DIR.exists():
|
||||
sys.path.insert(0, str(SUPERSET_DIR))
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Depends, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
import httpx
|
||||
from datetime import datetime
|
||||
import logging
|
||||
|
||||
# Configuração de logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Criar app FastAPI
|
||||
app = FastAPI(
|
||||
title="MetaSet BI",
|
||||
description="Apache Superset Fork for Arcádia Suite",
|
||||
version="4.1.0-arcadia",
|
||||
docs_url="/api/docs",
|
||||
redoc_url="/api/redoc",
|
||||
)
|
||||
|
||||
# CORS
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# =============================================================================
|
||||
# HEALTH CHECK (Kernel Integration)
|
||||
# =============================================================================
|
||||
@app.get("/health", tags=["health"])
|
||||
async def health_check():
|
||||
"""Health check endpoint for Kernel ServiceRegistry"""
|
||||
return {
|
||||
"status": "healthy",
|
||||
"service": "metaset",
|
||||
"version": "4.1.0-arcadia",
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"port": 8100,
|
||||
"checks": {
|
||||
"database": await _check_database(),
|
||||
"redis": await _check_redis(),
|
||||
}
|
||||
}
|
||||
|
||||
async def _check_database() -> dict:
|
||||
"""Check database connectivity"""
|
||||
try:
|
||||
# TODO: Implement actual DB check
|
||||
return {"status": "ok", "latency_ms": 0}
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
async def _check_redis() -> dict:
|
||||
"""Check Redis connectivity"""
|
||||
try:
|
||||
# TODO: Implement actual Redis check
|
||||
return {"status": "ok", "latency_ms": 0}
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
# =============================================================================
|
||||
# KERNEL INTEGRATION
|
||||
# =============================================================================
|
||||
@app.get("/api/kernel/info", tags=["kernel"])
|
||||
async def kernel_info():
|
||||
"""Return service info for Kernel discovery"""
|
||||
return {
|
||||
"id": "metaset",
|
||||
"name": "MetaSet BI",
|
||||
"type": "bi",
|
||||
"version": "4.1.0-arcadia",
|
||||
"capabilities": [
|
||||
"dashboards",
|
||||
"charts",
|
||||
"sql_lab",
|
||||
"datasets",
|
||||
"embedding",
|
||||
"rls"
|
||||
],
|
||||
"endpoints": {
|
||||
"health": "/health",
|
||||
"auth": "/api/auth",
|
||||
"dashboards": "/api/dashboards",
|
||||
"datasets": "/api/datasets",
|
||||
}
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# AUTH INTEGRATION (JWT Arcádia)
|
||||
# =============================================================================
|
||||
@app.post("/api/auth/arcadia", tags=["auth"])
|
||||
async def auth_arcadia(request: Request):
|
||||
"""
|
||||
Authenticate using Arcádia JWT token
|
||||
Creates/updates user in MetaSet from Arcádia payload
|
||||
"""
|
||||
try:
|
||||
body = await request.json()
|
||||
token = body.get("token")
|
||||
|
||||
if not token:
|
||||
raise HTTPException(status_code=400, detail="Token required")
|
||||
|
||||
# TODO: Validate JWT against Arcádia
|
||||
# TODO: Create/update user in Superset DB
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Authenticated via Arcádia",
|
||||
"token_type": "bearer",
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Auth error: {e}")
|
||||
raise HTTPException(status_code=401, detail="Authentication failed")
|
||||
|
||||
# =============================================================================
|
||||
# DASHBOARD API
|
||||
# =============================================================================
|
||||
@app.get("/api/dashboards", tags=["dashboards"])
|
||||
async def list_dashboards():
|
||||
"""List available dashboards"""
|
||||
# TODO: Query Superset database
|
||||
return {
|
||||
"dashboards": [],
|
||||
"count": 0
|
||||
}
|
||||
|
||||
@app.post("/api/dashboards", tags=["dashboards"])
|
||||
async def create_dashboard(request: Request):
|
||||
"""Create new dashboard"""
|
||||
try:
|
||||
body = await request.json()
|
||||
# TODO: Create in Superset
|
||||
return {"success": True, "id": None}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
# =============================================================================
|
||||
# GUEST TOKEN (EMBEDDING)
|
||||
# =============================================================================
|
||||
@app.post("/api/guest-token", tags=["embedding"])
|
||||
async def create_guest_token(request: Request):
|
||||
"""
|
||||
Create guest token for dashboard embedding
|
||||
Required for embedded Superset in Arcádia frontend
|
||||
"""
|
||||
try:
|
||||
body = await request.json()
|
||||
|
||||
# TODO: Generate JWT guest token
|
||||
# TODO: Validate user has access to requested resources
|
||||
|
||||
return {
|
||||
"token": "guest-token-placeholder",
|
||||
"expires_in": 300
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
# =============================================================================
|
||||
# TENANT MANAGEMENT (Multi-tenancy)
|
||||
# =============================================================================
|
||||
@app.get("/api/tenants/{tenant_id}/rls", tags=["tenants"])
|
||||
async def get_tenant_rls(tenant_id: int):
|
||||
"""Get Row Level Security policies for tenant"""
|
||||
return {
|
||||
"tenant_id": tenant_id,
|
||||
"policies": []
|
||||
}
|
||||
|
||||
@app.post("/api/tenants/{tenant_id}/rls", tags=["tenants"])
|
||||
async def set_tenant_rls(tenant_id: int, request: Request):
|
||||
"""Set RLS policies for tenant"""
|
||||
try:
|
||||
body = await request.json()
|
||||
# TODO: Configure RLS in Superset
|
||||
return {"success": True}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
# =============================================================================
|
||||
# SYNC API (FDB-Bridge Integration)
|
||||
# =============================================================================
|
||||
@app.post("/api/sync/trigger", tags=["sync"])
|
||||
async def trigger_sync(request: Request):
|
||||
"""Trigger data sync from FDB-Bridge"""
|
||||
try:
|
||||
body = await request.json()
|
||||
source = body.get("source", "firebird")
|
||||
|
||||
# TODO: Call FDB-Bridge API
|
||||
# TODO: Queue sync job in Celery
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"job_id": None,
|
||||
"source": source,
|
||||
"status": "queued"
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.get("/api/sync/status/{job_id}", tags=["sync"])
|
||||
async def sync_status(job_id: str):
|
||||
"""Get sync job status"""
|
||||
return {
|
||||
"job_id": job_id,
|
||||
"status": "completed",
|
||||
"progress": 100
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# MAIN
|
||||
# =============================================================================
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
port = int(os.environ.get("METASET_PORT", 8100))
|
||||
|
||||
uvicorn.run(
|
||||
"main:app",
|
||||
host="0.0.0.0",
|
||||
port=port,
|
||||
reload=os.environ.get("NODE_ENV") == "development"
|
||||
)
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
# MetaSet BI - Apache Superset Fork
|
||||
# Dependências compatíveis com apache-superset==4.1.0
|
||||
|
||||
apache-superset==4.1.0
|
||||
gunicorn>=22.0.0
|
||||
|
||||
# Database
|
||||
psycopg2-binary>=2.9.9
|
||||
SQLAlchemy>=1.4,<2
|
||||
alembic>=1.13.0
|
||||
|
||||
# Security
|
||||
PyJWT>=2.8.0
|
||||
|
||||
# Cache - compatível com apache-superset
|
||||
redis>=4.6.0,<5.0
|
||||
|
||||
# Fix: marshmallow 4+ quebra o superset 4.1.0
|
||||
marshmallow<4.0.0
|
||||
marshmallow-sqlalchemy<1.0.0
|
||||
|
||||
# Util
|
||||
python-dotenv>=1.0.0
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
MetaSet - Serviço Apache Superset integrado ao Arcadia Kernel
|
||||
Porta: 8100 (interna)
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
# Configurar logging para stdout (capturado pelo Kernel)
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='[MetaSet] %(levelname)s: %(message)s',
|
||||
handlers=[logging.StreamHandler(sys.stdout)]
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def setup_environment():
|
||||
"""Configurar variáveis de ambiente"""
|
||||
base_dir = Path(__file__).parent.absolute()
|
||||
|
||||
os.environ.setdefault('SUPERSET_HOME', str(base_dir / 'superset_home'))
|
||||
os.environ.setdefault('PYTHONPATH', str(base_dir))
|
||||
|
||||
(base_dir / 'superset_home').mkdir(exist_ok=True)
|
||||
(base_dir / 'superset_home' / 'uploads').mkdir(exist_ok=True)
|
||||
|
||||
return base_dir
|
||||
|
||||
def parse_arguments():
|
||||
"""Parsear argumentos da linha de comando"""
|
||||
parser = argparse.ArgumentParser(description='MetaSet BI Service')
|
||||
parser.add_argument('--port', type=int, default=8100, help='Porta do serviço')
|
||||
parser.add_argument('--host', type=str, default='127.0.0.1', help='Host do serviço')
|
||||
parser.add_argument('--workers', type=int, default=4, help='Número de workers')
|
||||
return parser.parse_args()
|
||||
|
||||
def create_app():
|
||||
"""Criar aplicação Flask do Superset"""
|
||||
try:
|
||||
from superset import create_app as create_superset_app
|
||||
app = create_superset_app()
|
||||
except ImportError:
|
||||
logger.error("Apache Superset não instalado. Rode: pip install apache-superset")
|
||||
sys.exit(1)
|
||||
|
||||
@app.route('/health')
|
||||
def health_check():
|
||||
return {
|
||||
'status': 'healthy',
|
||||
'service': 'metaset',
|
||||
'version': app.config.get('VERSION_STRING', '4.1.0-arcadia'),
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}, 200
|
||||
|
||||
@app.route('/info')
|
||||
def service_info():
|
||||
return {
|
||||
'name': 'MetaSet',
|
||||
'description': 'Business Intelligence by ArcadiaSuite',
|
||||
'port': args.port,
|
||||
'features': ['sql_lab', 'dashboards', 'charts', 'rls']
|
||||
}
|
||||
|
||||
@app.route('/static/assets/images/branding/metaset-logo-horiz.png')
|
||||
def serve_logo():
|
||||
from flask import send_file
|
||||
return send_file('/app/superset/static/assets/images/superset-logo-horiz.png', mimetype='image/png')
|
||||
|
||||
return app
|
||||
|
||||
def main():
|
||||
"""Entry point principal"""
|
||||
global args
|
||||
|
||||
args = parse_arguments()
|
||||
base_dir = setup_environment()
|
||||
|
||||
logger.info(f"Iniciando MetaSet em {args.host}:{args.port}")
|
||||
logger.info(f"Diretório base: {base_dir}")
|
||||
|
||||
try:
|
||||
from gunicorn.app.base import BaseApplication
|
||||
except ImportError:
|
||||
logger.error("Gunicorn não instalado. Rode: pip install gunicorn")
|
||||
sys.exit(1)
|
||||
|
||||
class MetaSetApplication(BaseApplication):
|
||||
def __init__(self, app, options=None):
|
||||
self.options = options or {}
|
||||
self.application = app
|
||||
super().__init__()
|
||||
|
||||
def load_config(self):
|
||||
for key, value in self.options.items():
|
||||
if key in self.cfg.settings and value is not None:
|
||||
self.cfg.set(key.lower(), value)
|
||||
|
||||
def load(self):
|
||||
return self.application
|
||||
|
||||
app = create_app()
|
||||
|
||||
options = {
|
||||
'bind': f'{args.host}:{args.port}',
|
||||
'workers': args.workers,
|
||||
'worker_class': 'gthread',
|
||||
'threads': 4,
|
||||
'timeout': 120,
|
||||
'keepalive': 5,
|
||||
'max_requests': 1000,
|
||||
'max_requests_jitter': 50,
|
||||
'accesslog': '-',
|
||||
'errorlog': '-',
|
||||
'loglevel': 'info',
|
||||
'preload_app': True,
|
||||
}
|
||||
|
||||
MetaSetApplication(app, options).run()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
"""
|
||||
ArcadiaSecurityManager - Integração Superset com Arcadia Kernel
|
||||
"""
|
||||
|
||||
from superset.security import SupersetSecurityManager
|
||||
from flask import g, request, current_app
|
||||
from sqlalchemy import text
|
||||
import jwt
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ArcadiaSecurityManager(SupersetSecurityManager):
|
||||
"""
|
||||
Security Manager integrado ao Arcadia Kernel
|
||||
- Extrai tenant e usuário do token JWT
|
||||
- Aplica RLS automaticamente
|
||||
- Sincroniza roles com ArcadiaSuite
|
||||
"""
|
||||
|
||||
ROLE_MAPPING = {
|
||||
'superadmin': 'Admin',
|
||||
'admin': 'Alpha',
|
||||
'analyst': 'Gamma',
|
||||
'viewer': 'Public',
|
||||
}
|
||||
|
||||
def before_request(self):
|
||||
"""Executado antes de cada requisição"""
|
||||
# Primeiro, executar o comportamento padrão do Superset
|
||||
super().before_request()
|
||||
|
||||
auth_header = request.headers.get('Authorization', '')
|
||||
if not auth_header.startswith('Bearer '):
|
||||
return
|
||||
|
||||
token = auth_header.replace('Bearer ', '')
|
||||
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
token,
|
||||
current_app.config.get('SECRET_KEY', 'default-key'),
|
||||
algorithms=['HS256']
|
||||
)
|
||||
|
||||
g.tenant_id = payload.get('tenant_id')
|
||||
g.user_id = payload.get('sub')
|
||||
g.user_role = payload.get('role', 'viewer')
|
||||
g.user_email = payload.get('email')
|
||||
|
||||
if g.tenant_id:
|
||||
try:
|
||||
from superset import db
|
||||
db.session.execute(
|
||||
text(f"SET LOCAL app.current_tenant = {g.tenant_id}")
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Erro ao aplicar RLS: {e}")
|
||||
|
||||
except jwt.ExpiredSignatureError:
|
||||
logger.warning("JWT token expirado")
|
||||
except jwt.InvalidTokenError:
|
||||
logger.warning("JWT token inválido")
|
||||
except Exception as e:
|
||||
logger.warning(f"Erro ao processar JWT: {e}")
|
||||
|
||||
def can_access_datasource(self, datasource):
|
||||
"""Verificar acesso ao datasource baseado no tenant"""
|
||||
tenant_id = getattr(g, 'tenant_id', None)
|
||||
if not tenant_id:
|
||||
return super().can_access_datasource(datasource)
|
||||
|
||||
ds_tenant = getattr(datasource, 'tenant_id', None)
|
||||
if ds_tenant and str(ds_tenant) != str(tenant_id):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def get_user_roles(self, user):
|
||||
"""Retornar roles mapeadas do Arcadia"""
|
||||
arcadia_role = getattr(g, 'user_role', None)
|
||||
if arcadia_role:
|
||||
superset_role = self.ROLE_MAPPING.get(arcadia_role, 'Gamma')
|
||||
return [superset_role]
|
||||
return super().get_user_roles(user)
|
||||
|
||||
def is_admin(self):
|
||||
"""Verificar se usuário é admin no Arcadia"""
|
||||
arcadia_role = getattr(g, 'user_role', None)
|
||||
if arcadia_role:
|
||||
return arcadia_role in ['superadmin', 'admin']
|
||||
return super().is_admin()
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
"""
|
||||
Configuração do MetaSet (Superset) para ArcadiaSuite
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
BASE_DIR = Path(__file__).parent.absolute()
|
||||
SUPERSET_HOME = BASE_DIR / 'superset_home'
|
||||
|
||||
# ==========================================
|
||||
# BRANDING - ARCADIASUITE
|
||||
# ==========================================
|
||||
|
||||
APP_NAME = "MetaSet"
|
||||
APP_ICON = "/static/assets/images/branding/metaset-logo-horiz.png"
|
||||
LOGO_TARGET_PATH = "/"
|
||||
LOGO_TOOLTIP = "MetaSet - Business Intelligence by ArcadiaSuite"
|
||||
|
||||
# Favicons
|
||||
FAVICONS = [
|
||||
{"href": "/static/assets/images/branding/favicon-16x16.png", "sizes": "16x16"},
|
||||
{"href": "/static/assets/images/branding/favicon-32x32.png", "sizes": "32x32"},
|
||||
]
|
||||
|
||||
# Cores ArcadiaSuite (alinhadas ao site principal)
|
||||
ARCADIA_PRIMARY = "#0ea5e9" # Azul celeste
|
||||
ARCADIA_SECONDARY = "#10b981" # Verde
|
||||
ARCADIA_ACCENT = "#f59e0b" # Laranja
|
||||
ARCADIA_BG = "#f8fafc" # Background claro
|
||||
ARCADIA_TEXT = "#1e293b" # Texto escuro
|
||||
|
||||
# Desabilitar cache de arquivos estáticos para forçar reload
|
||||
SEND_FILE_MAX_AGE_DEFAULT = 0
|
||||
|
||||
THEME_DEFAULT = {
|
||||
"token": {
|
||||
"brandAppName": APP_NAME,
|
||||
"brandLogoAlt": "MetaSet by ArcadiaSuite",
|
||||
"brandLogoUrl": APP_ICON,
|
||||
"brandLogoMargin": "16px 0",
|
||||
"brandLogoHeight": "28px",
|
||||
"colorPrimary": ARCADIA_PRIMARY,
|
||||
"colorLink": ARCADIA_PRIMARY,
|
||||
"colorSuccess": ARCADIA_SECONDARY,
|
||||
"colorWarning": ARCADIA_ACCENT,
|
||||
"colorError": "#ef4444",
|
||||
"colorInfo": "#3b82f6",
|
||||
"colorBgLayout": ARCADIA_BG,
|
||||
"colorBgContainer": "#ffffff",
|
||||
"colorText": ARCADIA_TEXT,
|
||||
"colorTextSecondary": "#64748b",
|
||||
"fontFamily": "Inter, -apple-system, BlinkMacSystemFont, sans-serif",
|
||||
"fontSize": 14,
|
||||
"borderRadius": 8,
|
||||
"borderRadiusLG": 12,
|
||||
"boxShadow": "0 1px 3px 0 rgba(0, 0, 0, 0.1)",
|
||||
"boxShadowSecondary": "0 4px 6px -1px rgba(0, 0, 0, 0.1)",
|
||||
},
|
||||
"algorithm": "default",
|
||||
}
|
||||
|
||||
THEME_DARK = {
|
||||
**THEME_DEFAULT,
|
||||
"token": {
|
||||
**THEME_DEFAULT["token"],
|
||||
"colorBgLayout": "#0f172a",
|
||||
"colorBgContainer": "#1e293b",
|
||||
"colorText": "#f1f5f9",
|
||||
},
|
||||
"algorithm": "dark",
|
||||
}
|
||||
|
||||
# ==========================================
|
||||
# SEGURANÇA & MULTI-TENANCY
|
||||
# ==========================================
|
||||
|
||||
from security_manager import ArcadiaSecurityManager
|
||||
CUSTOM_SECURITY_MANAGER = ArcadiaSecurityManager
|
||||
|
||||
AUTH_TYPE = 1
|
||||
WTF_CSRF_ENABLED = True
|
||||
WTF_CSRF_TIME_LIMIT = 3600
|
||||
SESSION_COOKIE_HTTPONLY = True
|
||||
SESSION_COOKIE_SECURE = False
|
||||
SESSION_COOKIE_SAMESITE = "Lax"
|
||||
|
||||
# ==========================================
|
||||
# BANCO DE DADOS
|
||||
# ==========================================
|
||||
|
||||
SQLALCHEMY_DATABASE_URI = os.getenv(
|
||||
'DATABASE_URL',
|
||||
'postgresql://arcadia:arcadia@localhost:5432/metaset_db'
|
||||
)
|
||||
|
||||
SQLALCHEMY_EXAMPLES_URI = 'postgresql://arcadia:arcadia@localhost:5432/arcadia_db'
|
||||
|
||||
SQLALCHEMY_ENGINE_OPTIONS = {
|
||||
"connect_args": {
|
||||
"options": "-c row_security=on"
|
||||
},
|
||||
"pool_pre_ping": True,
|
||||
"pool_recycle": 300,
|
||||
}
|
||||
|
||||
# ==========================================
|
||||
# FEATURE FLAGS
|
||||
# ==========================================
|
||||
|
||||
FEATURE_FLAGS = {
|
||||
"EMBEDDED_SUPERSET": True,
|
||||
"EMBEDDABLE_CHARTS": True,
|
||||
"DASHBOARD_RBAC": True,
|
||||
"SQLLAB_BACKEND_PERSISTENCE": True,
|
||||
"DASHBOARD_VIRTUALIZATION": True,
|
||||
"DRILL_BY": True,
|
||||
"CSS_TEMPLATES": True,
|
||||
"ENABLE_JAVASCRIPT_CONTROLS": False,
|
||||
"ALERT_REPORTS": False,
|
||||
}
|
||||
|
||||
# ==========================================
|
||||
# CACHE
|
||||
# ==========================================
|
||||
|
||||
CACHE_CONFIG = {
|
||||
"CACHE_TYPE": "RedisCache",
|
||||
"CACHE_REDIS_HOST": os.getenv("REDIS_HOST", "localhost"),
|
||||
"CACHE_REDIS_PORT": int(os.getenv("REDIS_PORT", "6379")),
|
||||
"CACHE_REDIS_DB": 1,
|
||||
"CACHE_DEFAULT_TIMEOUT": 300,
|
||||
}
|
||||
|
||||
# ==========================================
|
||||
# LOGGING
|
||||
# ==========================================
|
||||
|
||||
LOG_FORMAT = "%(asctime)s [MetaSet] %(levelname)s: %(message)s"
|
||||
LOG_LEVEL = "INFO" if os.getenv("FLASK_ENV") == "production" else "DEBUG"
|
||||
|
||||
# ==========================================
|
||||
# UPLOADS & EXPORT
|
||||
# ==========================================
|
||||
|
||||
UPLOAD_FOLDER = str(SUPERSET_HOME / "uploads")
|
||||
UPLOAD_CHUNK_SIZE = 4096
|
||||
|
||||
CSV_EXPORT = {"encoding": "utf-8-sig"}
|
||||
EXCEL_EXPORT = {}
|
||||
|
||||
ROW_LIMIT = 100000
|
||||
SQL_MAX_ROW = 100000
|
||||
DISPLAY_MAX_ROW = 10000
|
||||
|
||||
SQLLAB_TIMEOUT = 60
|
||||
SUPERSET_WEBSERVER_TIMEOUT = 60
|
||||
|
||||
MAPBOX_API_KEY = os.getenv("MAPBOX_API_KEY", "")
|
||||
|
||||
FAVICONS = [
|
||||
{"href": "/static/assets/images/branding/favicon-16x16.png", "sizes": "16x16"},
|
||||
{"href": "/static/assets/images/branding/favicon-32x32.png", "sizes": "32x32"},
|
||||
{"href": "/static/assets/images/branding/apple-touch-icon.png", "sizes": "180x180"},
|
||||
]
|
||||
|
|
@ -107,6 +107,89 @@ interface EngineStatus {
|
|||
responseTime?: number;
|
||||
details?: any;
|
||||
error?: string;
|
||||
// Registry integration
|
||||
source?: 'registry' | 'static' | 'external';
|
||||
registryId?: string;
|
||||
registrySource?: string;
|
||||
}
|
||||
|
||||
// Busca serviços do Registry do Kernel
|
||||
async function fetchRegistryServices(): Promise<any[] | null> {
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 3000);
|
||||
|
||||
const response = await fetch('http://localhost:5001/api/registry/services', {
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
clearTimeout(timeout);
|
||||
|
||||
if (!response.ok) return null;
|
||||
|
||||
const data = await response.json();
|
||||
return data.success && Array.isArray(data.data?.services) ? data.data.services : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Mapeia serviço do Registry para EngineStatus
|
||||
function mapRegistryToEngine(service: any): EngineStatus {
|
||||
const id = service.id || service.name;
|
||||
|
||||
// Mapear ID do Registry para nome do Engine Room
|
||||
const nameMap: Record<string, string> = {
|
||||
'python-contabil': 'contabil',
|
||||
'python-bi': 'bi-engine',
|
||||
'python-automation': 'automation-engine',
|
||||
'python-fisco': 'fisco',
|
||||
'python-fiscal': 'fisco',
|
||||
'node-communication': 'communication',
|
||||
'miroflow-communication': 'communication',
|
||||
'metaset-bi': 'metaset',
|
||||
'superset-bi': 'metaset',
|
||||
};
|
||||
|
||||
const name = nameMap[id] || nameMap[service.name] || id;
|
||||
|
||||
// Mapear categoria
|
||||
const catMap: Record<string, string> = {
|
||||
'fiscal': 'fiscal',
|
||||
'contabil': 'fiscal',
|
||||
'bi': 'data',
|
||||
'data': 'data',
|
||||
'automation': 'automation',
|
||||
'communication': 'intelligence',
|
||||
'intelligence': 'intelligence',
|
||||
'erp': 'erp',
|
||||
};
|
||||
|
||||
const category = catMap[service.category] || service.category || 'data';
|
||||
|
||||
// Mapear health status
|
||||
let status: 'online' | 'offline' | 'error' = 'offline';
|
||||
if (service.healthStatus === 'healthy') status = 'online';
|
||||
else if (service.healthStatus === 'unhealthy') status = 'error';
|
||||
|
||||
return {
|
||||
name,
|
||||
displayName: service.displayName || service.name,
|
||||
type: service.type || 'unknown',
|
||||
port: service.port || 0,
|
||||
category,
|
||||
description: service.description || `Serviço ${service.name}`,
|
||||
status,
|
||||
details: {
|
||||
endpoint: service.endpoint,
|
||||
health: service.healthStatus,
|
||||
version: service.version,
|
||||
capabilities: service.capabilities?.map((c: any) => c.name) || [],
|
||||
},
|
||||
source: 'registry',
|
||||
registryId: service.id,
|
||||
registrySource: service.metadata?.source,
|
||||
};
|
||||
}
|
||||
|
||||
// Health check direto de um serviço (modo Docker)
|
||||
|
|
@ -117,7 +200,7 @@ async function checkServiceHealth(engineName: string): Promise<EngineStatus> {
|
|||
const kernelId = ENGINE_TO_KERNEL_ID[engineName];
|
||||
|
||||
// DEBUG: Log da URL sendo usada
|
||||
console.log(`[DEBUG] checkServiceHealth(${engineName}) => URL: ${serviceUrl}, env: ${process.env.CONTABIL_PYTHON_URL || 'N/A'}`);
|
||||
// console.log(`[DEBUG] checkServiceHealth(${engineName}) => URL: ${serviceUrl}, env: ${process.env.CONTABIL_PYTHON_URL || 'N/A'}`);
|
||||
|
||||
if (!config || !serviceUrl) {
|
||||
return {
|
||||
|
|
@ -129,6 +212,7 @@ async function checkServiceHealth(engineName: string): Promise<EngineStatus> {
|
|||
description: `Serviço ${engineName}`,
|
||||
status: "offline",
|
||||
error: "Configuração não encontrada",
|
||||
source: 'static' as const,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,27 +24,77 @@
|
|||
"autoStart": true
|
||||
},
|
||||
{
|
||||
"id": "python-bi",
|
||||
"name": "BI Engine",
|
||||
"description": "Motor de Business Intelligence e análise de dados",
|
||||
"type": "python",
|
||||
"command": "python3",
|
||||
"args": ["-m", "uvicorn", "bi_engine.main:app", "--host", "0.0.0.0", "--port", "8004"],
|
||||
"cwd": "./python-service",
|
||||
"id": "bi-api",
|
||||
"name": "BI API Gateway",
|
||||
"type": "node",
|
||||
"command": "node",
|
||||
"args": ["dist/server/bi/index.js"],
|
||||
"cwd": ".",
|
||||
"port": 8004,
|
||||
"autoStart": true,
|
||||
"env": {
|
||||
"NODE_ENV": "production",
|
||||
"SERVICE_NAME": "bi-api",
|
||||
"PORT": "8004",
|
||||
"METASET_HOST": "127.0.0.1",
|
||||
"METASET_PORT": "8100",
|
||||
"FDB_BRIDGE_PORT": "8200"
|
||||
},
|
||||
"healthCheck": {
|
||||
"path": "/health",
|
||||
"path": "/bi/health",
|
||||
"interval": 30000,
|
||||
"timeout": 5000,
|
||||
"retries": 3
|
||||
},
|
||||
"env": {
|
||||
"PYTHONPATH": ".",
|
||||
"SERVICE_NAME": "bi",
|
||||
"LOG_LEVEL": "info"
|
||||
},
|
||||
"maxRestarts": 5,
|
||||
"autoStart": true
|
||||
"description": "Gateway de BI com proxy para MetaSet e FDB-Bridge"
|
||||
},
|
||||
{
|
||||
"id": "metaset",
|
||||
"name": "MetaSet BI",
|
||||
"type": "python",
|
||||
"command": "python",
|
||||
"args": ["server/bi/metaset/run.py", "--port=8100", "--host=127.0.0.1"],
|
||||
"cwd": ".",
|
||||
"port": 8100,
|
||||
"autoStart": false,
|
||||
"env": {
|
||||
"PYTHONPATH": "server/bi/metaset",
|
||||
"FLASK_ENV": "production",
|
||||
"SUPERSET_CONFIG_PATH": "server/bi/metaset/superset_config.py",
|
||||
"SUPERSET_SECRET_KEY": "${SUPERSET_SECRET_KEY}"
|
||||
},
|
||||
"healthCheck": {
|
||||
"path": "/health",
|
||||
"interval": 30000,
|
||||
"timeout": 10000,
|
||||
"retries": 3
|
||||
},
|
||||
"maxRestarts": 3,
|
||||
"description": "Apache Superset rebrandado como MetaSet - Gerenciado via Docker Discovery"
|
||||
},
|
||||
{
|
||||
"id": "fdb-bridge",
|
||||
"name": "FDB Bridge",
|
||||
"type": "python",
|
||||
"command": "python",
|
||||
"args": ["server/bi/fdb-bridge/main.py", "--port=8200"],
|
||||
"cwd": ".",
|
||||
"port": 8200,
|
||||
"autoStart": true,
|
||||
"env": {
|
||||
"PYTHONPATH": "server/bi/fdb-bridge",
|
||||
"FDB_HOST": "${FDB_HOST}",
|
||||
"FDB_PORT": "${FDB_PORT:-3050}"
|
||||
},
|
||||
"healthCheck": {
|
||||
"path": "/health",
|
||||
"interval": 60000,
|
||||
"timeout": 5000,
|
||||
"retries": 3
|
||||
},
|
||||
"maxRestarts": 3,
|
||||
"description": "Conector Firebird para sincronização de dados"
|
||||
},
|
||||
{
|
||||
"id": "python-automation",
|
||||
|
|
|
|||
Loading…
Reference in New Issue