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 (regenerated on install)
|
||||||
package-lock.json
|
package-lock.json
|
||||||
.claude/
|
.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:
|
redis:
|
||||||
image: redis:7-alpine
|
image: redis:7-alpine
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
|
||||||
- "6379:6379"
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "redis-cli", "ping"]
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
|
|
@ -71,6 +69,9 @@ services:
|
||||||
- .:/app
|
- .:/app
|
||||||
- /app/node_modules
|
- /app/node_modules
|
||||||
- /app/dist
|
- /app/dist
|
||||||
|
networks:
|
||||||
|
- arcadia
|
||||||
|
- coolify
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
@ -219,20 +220,105 @@ services:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
||||||
# ── Apache Superset (BI avançado) ────────────────────────────────────────────
|
# ── MetaSet BI (Apache Superset Fork) ────────────────────────────────────────
|
||||||
superset:
|
metaset:
|
||||||
image: apache/superset:4.1.0
|
build:
|
||||||
|
context: ./server/bi/metaset
|
||||||
|
dockerfile: Dockerfile
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
labels:
|
labels:
|
||||||
- "arcadia.discovery.enabled=true"
|
- "arcadia.discovery.enabled=true"
|
||||||
- "arcadia.name=MetaSet BI"
|
- "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.type=java"
|
||||||
- "arcadia.category=data"
|
- "arcadia.category=data"
|
||||||
- "arcadia.port=8088"
|
- "arcadia.port=8088"
|
||||||
- "arcadia.capabilities=dashboards,charts,sql_lab,datasets"
|
- "arcadia.capabilities=dashboards,charts,sql_lab,datasets"
|
||||||
- "arcadia.requires_ia=false"
|
- "arcadia.requires_ia=false"
|
||||||
- "arcadia.health_path=/health"
|
- "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"
|
- "arcadia.version=4.1.0"
|
||||||
environment:
|
environment:
|
||||||
SUPERSET_SECRET_KEY: ${SUPERSET_SECRET_KEY:-superset-secret-change-in-prod}
|
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/superset_config.py:/app/pythonpath/superset_config.py:ro
|
||||||
- ./docker/superset/init.sh:/app/docker/init.sh:ro
|
- ./docker/superset/init.sh:/app/docker/init.sh:ro
|
||||||
- superset_home:/app/superset_home
|
- superset_home:/app/superset_home
|
||||||
|
extra_hosts:
|
||||||
|
- "db:10.0.10.2"
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
@ -262,7 +350,6 @@ services:
|
||||||
start_period: 60s
|
start_period: 60s
|
||||||
networks:
|
networks:
|
||||||
- arcadia
|
- arcadia
|
||||||
profiles: [bi]
|
|
||||||
|
|
||||||
# ─────────────── PERFIL: ai (Soberania de IA) ────────────────────────────────
|
# ─────────────── PERFIL: ai (Soberania de IA) ────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -377,6 +464,7 @@ volumes:
|
||||||
pgdata:
|
pgdata:
|
||||||
open_webui_data:
|
open_webui_data:
|
||||||
superset_home:
|
superset_home:
|
||||||
|
metaset_home:
|
||||||
erpnext_db:
|
erpnext_db:
|
||||||
erpnext_sites:
|
erpnext_sites:
|
||||||
erpnext_logs:
|
erpnext_logs:
|
||||||
|
|
@ -386,3 +474,6 @@ volumes:
|
||||||
networks:
|
networks:
|
||||||
arcadia:
|
arcadia:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
coolify:
|
||||||
|
external: true
|
||||||
|
name: coolify
|
||||||
|
|
|
||||||
|
|
@ -136,6 +136,7 @@
|
||||||
"@types/connect-pg-simple": "^7.0.3",
|
"@types/connect-pg-simple": "^7.0.3",
|
||||||
"@types/express": "4.17.21",
|
"@types/express": "4.17.21",
|
||||||
"@types/express-session": "^1.18.0",
|
"@types/express-session": "^1.18.0",
|
||||||
|
"@types/http-proxy-middleware": "^0.19.3",
|
||||||
"@types/node": "^20.19.0",
|
"@types/node": "^20.19.0",
|
||||||
"@types/passport": "^1.0.16",
|
"@types/passport": "^1.0.16",
|
||||||
"@types/passport-local": "^1.0.38",
|
"@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;
|
responseTime?: number;
|
||||||
details?: any;
|
details?: any;
|
||||||
error?: string;
|
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)
|
// 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];
|
const kernelId = ENGINE_TO_KERNEL_ID[engineName];
|
||||||
|
|
||||||
// DEBUG: Log da URL sendo usada
|
// 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) {
|
if (!config || !serviceUrl) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -129,6 +212,7 @@ async function checkServiceHealth(engineName: string): Promise<EngineStatus> {
|
||||||
description: `Serviço ${engineName}`,
|
description: `Serviço ${engineName}`,
|
||||||
status: "offline",
|
status: "offline",
|
||||||
error: "Configuração não encontrada",
|
error: "Configuração não encontrada",
|
||||||
|
source: 'static' as const,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,27 +24,77 @@
|
||||||
"autoStart": true
|
"autoStart": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "python-bi",
|
"id": "bi-api",
|
||||||
"name": "BI Engine",
|
"name": "BI API Gateway",
|
||||||
"description": "Motor de Business Intelligence e análise de dados",
|
"type": "node",
|
||||||
"type": "python",
|
"command": "node",
|
||||||
"command": "python3",
|
"args": ["dist/server/bi/index.js"],
|
||||||
"args": ["-m", "uvicorn", "bi_engine.main:app", "--host", "0.0.0.0", "--port", "8004"],
|
"cwd": ".",
|
||||||
"cwd": "./python-service",
|
|
||||||
"port": 8004,
|
"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": {
|
"healthCheck": {
|
||||||
"path": "/health",
|
"path": "/bi/health",
|
||||||
"interval": 30000,
|
"interval": 30000,
|
||||||
"timeout": 5000,
|
"timeout": 5000,
|
||||||
"retries": 3
|
"retries": 3
|
||||||
},
|
},
|
||||||
"env": {
|
|
||||||
"PYTHONPATH": ".",
|
|
||||||
"SERVICE_NAME": "bi",
|
|
||||||
"LOG_LEVEL": "info"
|
|
||||||
},
|
|
||||||
"maxRestarts": 5,
|
"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",
|
"id": "python-automation",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue