diff --git a/.gitignore b/.gitignore index 0483d65..9b2414f 100644 --- a/.gitignore +++ b/.gitignore @@ -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.* diff --git a/docker-compose.yml b/docker-compose.yml index ca8a4ff..8b72556 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/package.json b/package.json index dc1d0a4..cf6ab76 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/server/bi/fdb-bridge/main.py b/server/bi/fdb-bridge/main.py new file mode 100644 index 0000000..0dd553f --- /dev/null +++ b/server/bi/fdb-bridge/main.py @@ -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() diff --git a/server/bi/fdb-bridge/requirements.txt b/server/bi/fdb-bridge/requirements.txt new file mode 100644 index 0000000..1909a5c --- /dev/null +++ b/server/bi/fdb-bridge/requirements.txt @@ -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 diff --git a/server/bi/index.ts b/server/bi/index.ts new file mode 100644 index 0000000..fde0076 --- /dev/null +++ b/server/bi/index.ts @@ -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 = { + '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; diff --git a/server/bi/metaset/Dockerfile b/server/bi/metaset/Dockerfile new file mode 100644 index 0000000..cc7b2a7 --- /dev/null +++ b/server/bi/metaset/Dockerfile @@ -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"] diff --git a/server/bi/metaset/branding/favicon-16x16.png b/server/bi/metaset/branding/favicon-16x16.png new file mode 100644 index 0000000..54c44b6 Binary files /dev/null and b/server/bi/metaset/branding/favicon-16x16.png differ diff --git a/server/bi/metaset/branding/favicon-32x32.png b/server/bi/metaset/branding/favicon-32x32.png new file mode 100644 index 0000000..bf2d785 Binary files /dev/null and b/server/bi/metaset/branding/favicon-32x32.png differ diff --git a/server/bi/metaset/branding/metaset-icon.png b/server/bi/metaset/branding/metaset-icon.png new file mode 100644 index 0000000..05b222c Binary files /dev/null and b/server/bi/metaset/branding/metaset-icon.png differ diff --git a/server/bi/metaset/branding/metaset-logo-horiz.png b/server/bi/metaset/branding/metaset-logo-horiz.png new file mode 100644 index 0000000..82ea02d Binary files /dev/null and b/server/bi/metaset/branding/metaset-logo-horiz.png differ diff --git a/server/bi/metaset/config/__init__.py b/server/bi/metaset/config/__init__.py new file mode 100644 index 0000000..dd6c173 --- /dev/null +++ b/server/bi/metaset/config/__init__.py @@ -0,0 +1 @@ +# MetaSet Configuration Module diff --git a/server/bi/metaset/config/metaset_config.py b/server/bi/metaset/config/metaset_config.py new file mode 100644 index 0000000..1266448 --- /dev/null +++ b/server/bi/metaset/config/metaset_config.py @@ -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" diff --git a/server/bi/metaset/init.sh b/server/bi/metaset/init.sh new file mode 100644 index 0000000..12fb91e --- /dev/null +++ b/server/bi/metaset/init.sh @@ -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 diff --git a/server/bi/metaset/main.py b/server/bi/metaset/main.py new file mode 100644 index 0000000..7d49961 --- /dev/null +++ b/server/bi/metaset/main.py @@ -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" + ) diff --git a/server/bi/metaset/requirements.txt b/server/bi/metaset/requirements.txt new file mode 100644 index 0000000..3a3e4b2 --- /dev/null +++ b/server/bi/metaset/requirements.txt @@ -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 diff --git a/server/bi/metaset/run.py b/server/bi/metaset/run.py new file mode 100644 index 0000000..f6d64e2 --- /dev/null +++ b/server/bi/metaset/run.py @@ -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() diff --git a/server/bi/metaset/security_manager.py b/server/bi/metaset/security_manager.py new file mode 100644 index 0000000..99f264b --- /dev/null +++ b/server/bi/metaset/security_manager.py @@ -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() diff --git a/server/bi/metaset/superset_config.py b/server/bi/metaset/superset_config.py new file mode 100644 index 0000000..ed0029c --- /dev/null +++ b/server/bi/metaset/superset_config.py @@ -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"}, +] diff --git a/server/engine-room/kernel-adapter.ts b/server/engine-room/kernel-adapter.ts index d51eb1c..d7270e3 100644 --- a/server/engine-room/kernel-adapter.ts +++ b/server/engine-room/kernel-adapter.ts @@ -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 { + 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 = { + '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 = { + '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 { 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 { description: `Serviço ${engineName}`, status: "offline", error: "Configuração não encontrada", + source: 'static' as const, }; } diff --git a/server/kernel/config/services.json b/server/kernel/config/services.json index 0885ebd..3e34379 100644 --- a/server/kernel/config/services.json +++ b/server/kernel/config/services.json @@ -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",