#!/usr/bin/env python3 """Automated Metabase setup for Nexus Autoparts KPIs. Performs first-time setup if needed, then creates: - PostgreSQL database connection (master + tenant template) - Collection "Nexus KPIs" - Pre-built questions (cards) for common refaccionaria metrics - Dashboard grouping those cards Usage: export METABASE_URL=http://localhost:3000 export METABASE_ADMIN_EMAIL=admin@nexus.local export METABASE_ADMIN_PASS=changeme123 export MASTER_DB_URL=postgresql://nexus:pass@localhost/nexus_autoparts python3 scripts/setup_metabase.py """ import os import sys import json import time import requests METABASE_URL = os.environ.get('METABASE_URL', 'http://localhost:3000').rstrip('/') ADMIN_EMAIL = os.environ.get('METABASE_ADMIN_EMAIL', 'admin@nexus.local') ADMIN_PASS = os.environ.get('METABASE_ADMIN_PASS', '') MASTER_DB_URL = os.environ.get('MASTER_DB_URL', '') DB_CONFIG_PATH = os.path.expanduser('~/.nexus_metabase_config.json') def _get(url, session=None): headers = {} if session: headers['X-Metabase-Session'] = session r = requests.get(url, headers=headers) return r def _post(url, data, session=None): headers = {'Content-Type': 'application/json'} if session: headers['X-Metabase-Session'] = session r = requests.post(url, headers=headers, json=data) return r def get_setup_token(): r = _get(f"{METABASE_URL}/api/session/properties") return r.json().get('setup-token') def do_setup(token): """Create first admin user and initial DB connection.""" if not ADMIN_PASS: print("ERROR: METABASE_ADMIN_PASS is required for first-time setup.") sys.exit(1) # Parse DB connection db_name = 'nexus_autoparts' db_user = 'nexus' db_pass = '' if MASTER_DB_URL: # postgresql://user:pass@host/db rest = MASTER_DB_URL.replace('postgresql://', '') if '@' in rest: auth, host_db = rest.split('@', 1) if ':' in auth: db_user, db_pass = auth.split(':', 1) else: db_user = auth if '/' in host_db: host_port, db_name = host_db.split('/', 1) else: host_port = host_db host = host_port.split(':')[0] else: host = 'localhost' else: host = 'host.docker.internal' payload = { 'token': token, 'user': { 'first_name': 'Admin', 'last_name': 'Nexus', 'email': ADMIN_EMAIL, 'site_name': 'Nexus Autoparts', 'password': ADMIN_PASS, }, 'prefs': { 'site_name': 'Nexus Autoparts', 'site_locale': 'es', 'allow_tracking': False, }, 'database': { 'engine': 'postgres', 'name': 'Nexus Master DB', 'details': { 'host': host, 'port': 5432, 'dbname': db_name, 'user': db_user, 'password': db_pass, 'ssl': False, 'tunnel-enabled': False, }, 'auto_run_queries': True, 'is_full_sync': True, 'is_on_demand': False, } } r = _post(f"{METABASE_URL}/api/setup", payload) if r.status_code not in (200, 201): print(f"Setup failed: {r.status_code} {r.text}") sys.exit(1) data = r.json() session_id = data.get('id') print(f"Setup complete. Admin user created: {ADMIN_EMAIL}") return session_id def login(): """Login existing user.""" r = _post(f"{METABASE_URL}/api/session", { 'username': ADMIN_EMAIL, 'password': ADMIN_PASS, }) if r.status_code != 200: print(f"Login failed: {r.status_code} {r.text}") return None return r.json().get('id') def get_or_create_collection(session, name="Nexus KPIs"): """Get existing collection or create new.""" r = _get(f"{METABASE_URL}/api/collection", session) for c in r.json(): if c.get('name') == name: return c['id'] r = _post(f"{METABASE_URL}/api/collection", { 'name': name, 'color': '#509EE3', 'description': 'Dashboards y métricas para Nexus Autoparts' }, session) return r.json()['id'] def create_question(session, collection_id, name, sql, display='table', visualization_settings=None): """Create a native SQL question (card).""" payload = { 'name': name, 'dataset_query': { 'type': 'native', 'native': { 'query': sql, 'template-tags': {}, }, 'database': None, # Will be set from first available DB }, 'display': display, 'collection_id': collection_id, 'visualization_settings': visualization_settings or {}, } # Find first available database dbs = _get(f"{METABASE_URL}/api/database", session).json() if 'data' in dbs and dbs['data']: payload['dataset_query']['database'] = dbs['data'][0]['id'] else: print("WARNING: No databases found in Metabase. Skipping question creation.") return None r = _post(f"{METABASE_URL}/api/card", payload, session) if r.status_code in (200, 201): return r.json()['id'] print(f"Card creation failed ({name}): {r.status_code} {r.text[:200]}") return None def create_dashboard(session, collection_id, name): """Create a dashboard.""" r = _post(f"{METABASE_URL}/api/dashboard", { 'name': name, 'collection_id': collection_id, 'description': 'KPIs principales de la refaccionaria', }, session) if r.status_code in (200, 201): return r.json()['id'] print(f"Dashboard creation failed: {r.status_code} {r.text[:200]}") return None def add_card_to_dashboard(session, dashboard_id, card_id, row, col, size_x=6, size_y=4): """Add a card to a dashboard grid.""" payload = { 'cardId': card_id, 'row': row, 'col': col, 'sizeX': size_x, 'sizeY': size_y, } r = _post(f"{METABASE_URL}/api/dashboard/{dashboard_id}/cards", payload, session) return r.status_code in (200, 201) def main(): print("Nexus Autoparts — Metabase Setup") print("=" * 50) # Verify Metabase is reachable try: health = requests.get(f"{METABASE_URL}/api/health").json() if health.get('status') != 'ok': print("ERROR: Metabase is not ready.") sys.exit(1) except Exception as e: print(f"ERROR: Cannot reach Metabase: {e}") sys.exit(1) # Determine if first-time setup is needed token = get_setup_token() session = None if token: print("First-time setup detected...") session = do_setup(token) else: print("Metabase already set up. Logging in...") if not ADMIN_PASS: print("ERROR: METABASE_ADMIN_PASS required to login.") sys.exit(1) session = login() if not session: print("ERROR: Could not obtain Metabase session.") sys.exit(1) # Save config config = { 'metabase_url': METABASE_URL, 'admin_email': ADMIN_EMAIL, 'session_id': session, } with open(DB_CONFIG_PATH, 'w') as f: json.dump(config, f) os.chmod(DB_CONFIG_PATH, 0o600) # Create collection coll_id = get_or_create_collection(session) print(f"Collection ID: {coll_id}") # Create questions questions = [] q1 = create_question(session, coll_id, "Ventas por día (últimos 30 días)", """ SELECT DATE(created_at) as fecha, COUNT(*) as ventas, SUM(total) as total FROM sales WHERE status = 'completed' AND created_at >= CURRENT_DATE - INTERVAL '30 days' GROUP BY fecha ORDER BY fecha DESC; """, display='line', visualization_settings={"graph.dimensions":["fecha"],"graph.metrics":["ventas","total"]} ) if q1: questions.append((q1, 0, 0, 6, 4)) q2 = create_question(session, coll_id, "Top 10 productos vendidos", """ SELECT si.part_number, si.name, SUM(si.quantity) as cantidad, SUM(si.subtotal) as revenue FROM sale_items si JOIN sales s ON si.sale_id = s.id WHERE s.status = 'completed' GROUP BY si.part_number, si.name ORDER BY cantidad DESC LIMIT 10; """, display='bar' ) if q2: questions.append((q2, 0, 6, 6, 4)) q3 = create_question(session, coll_id, "Stock bajo (reorder alerts abiertas)", """ SELECT i.part_number, i.name, ra.stock_at_alert, ra.threshold, b.name as branch FROM reorder_alerts ra JOIN inventory i ON ra.inventory_id = i.id LEFT JOIN branches b ON ra.branch_id = b.id WHERE ra.status = 'open' ORDER BY ra.stock_at_alert ASC; """ ) if q3: questions.append((q3, 4, 0, 6, 4)) q4 = create_question(session, coll_id, "Ventas por sucursal (este mes)", """ SELECT b.name as sucursal, COUNT(*) as ventas, SUM(s.total) as total FROM sales s LEFT JOIN branches b ON s.branch_id = b.id WHERE s.status = 'completed' AND s.created_at >= DATE_TRUNC('month', CURRENT_DATE) GROUP BY sucursal ORDER BY total DESC; """, display='pie' ) if q4: questions.append((q4, 4, 6, 6, 4)) q5 = create_question(session, coll_id, "Clientes con más compras", """ SELECT c.name, COUNT(*) as compras, SUM(s.total) as total FROM sales s JOIN customers c ON s.customer_id = c.id WHERE s.status = 'completed' GROUP BY c.name ORDER BY total DESC LIMIT 10; """ ) if q5: questions.append((q5, 8, 0, 6, 4)) q6 = create_question(session, coll_id, "Margen de ganancia por producto", """ SELECT si.name, SUM(si.unit_cost * si.quantity) as costo_total, SUM(si.subtotal) as venta_total, ROUND(((SUM(si.subtotal) - SUM(si.unit_cost * si.quantity)) / NULLIF(SUM(si.subtotal), 0)) * 100, 2) as margen_pct FROM sale_items si JOIN sales s ON si.sale_id = s.id WHERE s.status = 'completed' GROUP BY si.name HAVING SUM(si.subtotal) > 0 ORDER BY margen_pct DESC LIMIT 10; """ ) if q6: questions.append((q6, 8, 6, 6, 4)) # Create dashboard if questions: dash_id = create_dashboard(session, coll_id, "Nexus KPIs — Panel Principal") if dash_id: for card_id, row, col, sx, sy in questions: add_card_to_dashboard(session, dash_id, card_id, row, col, sx, sy) print(f"Dashboard created: {METABASE_URL}/dashboard/{dash_id}") else: print("Dashboard creation skipped.") else: print("No questions created (no database connected yet).") print("\nDone. Access Metabase at:", METABASE_URL) print(f"Admin email: {ADMIN_EMAIL}") print(f"Config saved to: {DB_CONFIG_PATH}") if __name__ == '__main__': main()