FASE 4: - Redis cache de stock con fallback graceful - Multi-moneda (MXN/USD) con contabilidad en MXN - Proveedores y ordenes de compra completo - Meilisearch 1.5M+ partes indexadas - Metabase KPIs con dashboard auto-generado FASE 5: - CRM mejorado: activities, tags, loyalty program, analytics - Imagenes de partes: upload, resize, thumbnails WebP - Ordenes de servicio Kanban: received->diagnosis->repair->ready->delivered - Garantias/RMA, alertas de reorden, multi-sucursal - Stubs BNPL (APLAZO) y ERP Sync (Aspel/Contpaqi) FASE 6: - Notificaciones automaticas: push/WhatsApp/email/in-app - Reportes de ahorro vs retail_price - Logistica + tracking: DHL, FedEx, Estafeta, 99min, Uber - API Publica: API keys, rate limiting, catalog search Migraciones: v1.9-v3.0 Tests: 93/93 pasando Backup: nexus_backup_20260427_045859.tar.gz
223 lines
6.9 KiB
Python
Executable File
223 lines
6.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Nexus Autoparts — Post-Installation Health Check
|
|
|
|
Verifies that all components are running correctly after installation.
|
|
Usage: python3 scripts/health_check.py
|
|
"""
|
|
|
|
import sys
|
|
import os
|
|
import requests
|
|
import psycopg2
|
|
import redis
|
|
|
|
# Ensure we can import from project root
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
|
|
def info(msg):
|
|
print(f"[INFO] {msg}")
|
|
|
|
|
|
def ok(msg):
|
|
print(f"[OK] {msg}")
|
|
|
|
|
|
def fail(msg):
|
|
print(f"[FAIL] {msg}")
|
|
return False
|
|
|
|
|
|
def check_postgresql():
|
|
"""Verify PostgreSQL is running and accessible."""
|
|
info("Checking PostgreSQL...")
|
|
try:
|
|
conn = psycopg2.connect("postgresql://nexus@localhost/nexus_autoparts")
|
|
cur = conn.cursor()
|
|
cur.execute("SELECT version()")
|
|
version = cur.fetchone()[0]
|
|
cur.close()
|
|
conn.close()
|
|
ok(f"PostgreSQL running: {version.split()[0]} {version.split()[1]}")
|
|
return True
|
|
except Exception as e:
|
|
return fail(f"PostgreSQL connection failed: {e}")
|
|
|
|
|
|
def check_master_db():
|
|
"""Verify master DB has required tables."""
|
|
info("Checking master database schema...")
|
|
try:
|
|
conn = psycopg2.connect("postgresql://nexus@localhost/nexus_autoparts")
|
|
cur = conn.cursor()
|
|
cur.execute("""
|
|
SELECT table_name FROM information_schema.tables
|
|
WHERE table_schema = 'public'
|
|
AND table_name IN ('tenants', 'brands', 'models', 'years', 'part_categories')
|
|
""")
|
|
tables = {r[0] for r in cur.fetchall()}
|
|
cur.close()
|
|
conn.close()
|
|
|
|
required = {'tenants', 'brands', 'models', 'years', 'part_categories'}
|
|
missing = required - tables
|
|
if missing:
|
|
return fail(f"Missing master tables: {missing}")
|
|
ok("Master database schema is complete")
|
|
return True
|
|
except Exception as e:
|
|
return fail(f"Master DB check failed: {e}")
|
|
|
|
|
|
def check_tenant_template():
|
|
"""Verify tenant_template database exists."""
|
|
info("Checking tenant_template database...")
|
|
try:
|
|
conn = psycopg2.connect("postgresql://nexus@localhost/tenant_template")
|
|
cur = conn.cursor()
|
|
cur.execute("SELECT 1 FROM pg_tables WHERE tablename = 'sales'")
|
|
if not cur.fetchone():
|
|
return fail("tenant_template missing 'sales' table")
|
|
cur.close()
|
|
conn.close()
|
|
ok("tenant_template database exists and has core tables")
|
|
return True
|
|
except Exception as e:
|
|
return fail(f"tenant_template check failed: {e}")
|
|
|
|
|
|
def check_first_tenant():
|
|
"""Verify at least one tenant exists."""
|
|
info("Checking first tenant...")
|
|
try:
|
|
conn = psycopg2.connect("postgresql://nexus@localhost/nexus_autoparts")
|
|
cur = conn.cursor()
|
|
cur.execute("SELECT COUNT(*) FROM tenants WHERE is_active = true")
|
|
count = cur.fetchone()[0]
|
|
cur.close()
|
|
conn.close()
|
|
|
|
if count == 0:
|
|
return fail("No active tenants found")
|
|
ok(f"Found {count} active tenant(s)")
|
|
return True
|
|
except Exception as e:
|
|
return fail(f"Tenant check failed: {e}")
|
|
|
|
|
|
def check_pos_health():
|
|
"""Verify POS health endpoint responds."""
|
|
info("Checking POS health endpoint...")
|
|
try:
|
|
resp = requests.get("http://localhost:5001/pos/health", timeout=5)
|
|
if resp.status_code == 200 and resp.json().get("status") == "ok":
|
|
ok("POS health endpoint is responding")
|
|
return True
|
|
return fail(f"POS health returned: {resp.status_code} {resp.text}")
|
|
except requests.exceptions.ConnectionError:
|
|
return fail("POS not running on port 5001")
|
|
except Exception as e:
|
|
return fail(f"POS health check failed: {e}")
|
|
|
|
|
|
def check_redis():
|
|
"""Verify Redis is running and accessible."""
|
|
info("Checking Redis...")
|
|
try:
|
|
r = redis.from_url(
|
|
os.environ.get('REDIS_URL', 'redis://localhost:6379/0'),
|
|
decode_responses=True
|
|
)
|
|
if r.ping():
|
|
info = r.info('server')
|
|
ok(f"Redis {info.get('redis_version', '?')} running")
|
|
return True
|
|
return fail("Redis PING returned False")
|
|
except Exception as e:
|
|
return fail(f"Redis connection failed: {e}")
|
|
|
|
|
|
def check_meilisearch():
|
|
"""Verify Meilisearch is running."""
|
|
info("Checking Meilisearch...")
|
|
try:
|
|
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'pos'))
|
|
from services.meili_search import health_check
|
|
if health_check():
|
|
ok("Meilisearch running")
|
|
return True
|
|
return fail("Meilisearch health check failed")
|
|
except Exception as e:
|
|
return fail(f"Meilisearch connection failed: {e}")
|
|
|
|
|
|
def check_metabase():
|
|
"""Verify Metabase is running."""
|
|
info("Checking Metabase...")
|
|
try:
|
|
import requests
|
|
url = os.environ.get('METABASE_URL', 'http://localhost:3000')
|
|
r = requests.get(f"{url}/api/health", timeout=5)
|
|
if r.status_code == 200 and r.json().get('status') == 'ok':
|
|
ok("Metabase running")
|
|
return True
|
|
return fail(f"Metabase returned: {r.status_code}")
|
|
except Exception as e:
|
|
return fail(f"Metabase connection failed: {e}")
|
|
|
|
|
|
def check_web_health():
|
|
"""Verify web/dashboard responds."""
|
|
info("Checking web dashboard...")
|
|
try:
|
|
resp = requests.get("http://localhost:5000/", timeout=5)
|
|
if resp.status_code == 200:
|
|
ok("Web dashboard is responding")
|
|
return True
|
|
return fail(f"Web returned status: {resp.status_code}")
|
|
except requests.exceptions.ConnectionError:
|
|
return fail("Web server not running on port 5000")
|
|
except Exception as e:
|
|
return fail(f"Web check failed: {e}")
|
|
|
|
|
|
def main():
|
|
print("=" * 60)
|
|
print(" Nexus Autoparts — Health Check")
|
|
print("=" * 60)
|
|
print()
|
|
|
|
results = []
|
|
results.append(("PostgreSQL", check_postgresql()))
|
|
results.append(("Redis Cache", check_redis()))
|
|
results.append(("Meilisearch", check_meilisearch()))
|
|
results.append(("Metabase", check_metabase()))
|
|
results.append(("Master DB Schema", check_master_db()))
|
|
results.append(("Tenant Template", check_tenant_template()))
|
|
results.append(("First Tenant", check_first_tenant()))
|
|
results.append(("POS Health", check_pos_health()))
|
|
results.append(("Web Dashboard", check_web_health()))
|
|
|
|
print()
|
|
print("=" * 60)
|
|
passed = sum(1 for _, r in results if r)
|
|
total = len(results)
|
|
print(f" Results: {passed}/{total} checks passed")
|
|
print("=" * 60)
|
|
|
|
if passed < total:
|
|
print()
|
|
print("Failed checks:")
|
|
for name, result in results:
|
|
if not result:
|
|
print(f" - {name}")
|
|
sys.exit(1)
|
|
else:
|
|
print()
|
|
print("All systems operational!")
|
|
sys.exit(0)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|