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:
Jonas Pacheco 2026-04-07 16:04:09 -03:00
parent 1f23256032
commit 465775ec99
21 changed files with 1589 additions and 22 deletions

9
.gitignore vendored
View File

@ -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.*

View File

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

View File

@ -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",

View File

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

View File

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

160
server/bi/index.ts Normal file
View File

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

View File

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

View File

@ -0,0 +1 @@
# MetaSet Configuration Module

View File

@ -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"

121
server/bi/metaset/init.sh Normal file
View File

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

242
server/bi/metaset/main.py Normal file
View File

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

View File

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

126
server/bi/metaset/run.py Normal file
View File

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

View File

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

View File

@ -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"},
]

View File

@ -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,
};
}

View File

@ -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",