feat: app script_navegador y dashboard Metabase

App Go para ejecutar scripts de navegación automatizada usando las
funciones CDP del registry. Incluye script de creación de dashboard
en Metabase para monitoreo.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-30 14:24:39 +02:00
parent 9ed0f2e16f
commit 3c250a9252
15 changed files with 1322 additions and 0 deletions
@@ -0,0 +1,195 @@
"""Crea un dashboard en Metabase para monitorear operations de script_navegador."""
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "python", "functions"))
from metabase.client import metabase_auth
from metabase import (
metabase_list_databases,
metabase_create_card,
metabase_create_dashboard,
metabase_update_dashboard,
metabase_list_dashboards,
)
from metabase.databases import metabase_add_database
# --- Config ---
METABASE_URL = "http://localhost:3000"
EMAIL = "admin@fnregistry.local"
PASSWORD = "FnRegistry2024!"
# Path de operations.db dentro del contenedor Docker
# Copiar con: docker exec metabase mkdir -p /data/ops-script-navegador && docker cp apps/script_navegador/operations.db metabase:/data/ops-script-navegador/operations.db
OPS_DB_PATH = "/data/ops-script-navegador/operations.db"
DB_NAME = "ops-script-navegador"
CARDS = [
# ---- Fila 0: KPIs (h=5) ----
{
"name": "Total Ejecuciones",
"display": "scalar",
"sql": "SELECT COUNT(*) AS total FROM executions;",
"size_x": 6, "size_y": 5, "col": 0, "row": 0,
},
{
"name": "Ejecuciones Exitosas",
"display": "scalar",
"sql": "SELECT COUNT(*) AS exitosas FROM executions WHERE status = 'success';",
"size_x": 6, "size_y": 5, "col": 6, "row": 0,
},
{
"name": "Ejecuciones Fallidas",
"display": "scalar",
"sql": "SELECT COUNT(*) AS fallidas FROM executions WHERE status = 'failure';",
"size_x": 6, "size_y": 5, "col": 12, "row": 0,
},
{
"name": "Duracion Promedio (ms)",
"display": "scalar",
"sql": "SELECT ROUND(AVG(duration_ms)) AS avg_ms FROM executions WHERE status = 'success';",
"size_x": 6, "size_y": 5, "col": 18, "row": 0,
},
# ---- Fila 5: Tendencias (h=8) ----
{
"name": "Ejecuciones por Estado",
"display": "pie",
"sql": "SELECT status, COUNT(*) AS cantidad FROM executions GROUP BY status;",
"size_x": 8, "size_y": 8, "col": 0, "row": 5,
},
{
"name": "Duracion por Ejecucion (timeline)",
"display": "line",
"sql": """
SELECT
started_at,
duration_ms,
status
FROM executions
ORDER BY started_at;
""",
"size_x": 16, "size_y": 8, "col": 8, "row": 5,
},
# ---- Fila 13: Detalle de pasos (h=9) ----
{
"name": "Pasos por Script (metricas)",
"display": "table",
"sql": """
SELECT
id,
status,
records_in AS pasos_total,
records_out AS pasos_exitosos,
duration_ms,
CASE WHEN error = '' THEN '-' ELSE error END AS error,
json_extract(metrics, '$.script_name') AS script,
started_at
FROM executions
ORDER BY started_at DESC
LIMIT 20;
""",
"size_x": 24, "size_y": 9, "col": 0, "row": 13,
},
# ---- Fila 22: Logs (h=9) ----
{
"name": "Logs Recientes",
"display": "table",
"sql": """
SELECT
level,
source,
message,
json_extract(metadata, '$.action') AS action,
json_extract(metadata, '$.elapsed_ms') AS elapsed_ms,
created_at
FROM logs
ORDER BY created_at DESC
LIMIT 50;
""",
"size_x": 24, "size_y": 9, "col": 0, "row": 22,
},
]
DASHBOARD_NAME = "script_navegador Operations"
def main():
print("Autenticando en Metabase...")
client = metabase_auth(METABASE_URL, EMAIL, PASSWORD)
# Buscar si ya existe la database
dbs = metabase_list_databases(client)
ops_db_id = None
for db in dbs:
if db.get("name") == DB_NAME:
ops_db_id = db["id"]
print(f" Database ya existe: {DB_NAME} (id={ops_db_id})")
break
if not ops_db_id:
print(f"Registrando {DB_NAME} como datasource SQLite ({OPS_DB_PATH})...")
new_db = metabase_add_database(
client=client,
name=DB_NAME,
engine="sqlite",
details={"db": OPS_DB_PATH},
)
ops_db_id = new_db["id"]
print(f" Database registrada: id={ops_db_id}")
# Eliminar dashboard existente si lo hay
existing = metabase_list_dashboards(client)
for d in existing:
if d.get("name") == DASHBOARD_NAME:
print(f" Dashboard ya existe (id={d['id']}), recreando...")
from metabase import metabase_delete_dashboard
metabase_delete_dashboard(client, d["id"])
# Crear cards
print("Creando cards...")
created_cards = []
for i, card_def in enumerate(CARDS):
card = metabase_create_card(
client,
name=card_def["name"],
dataset_query={
"database": ops_db_id,
"type": "native",
"native": {"query": card_def["sql"]},
},
display=card_def["display"],
description=f"script_navegador: {card_def['name']}",
)
created_cards.append((card, card_def))
print(f" [{i+1}/{len(CARDS)}] {card_def['name']} (id={card['id']})")
# Crear dashboard
print("Creando dashboard...")
dashboard = metabase_create_dashboard(
client,
name=DASHBOARD_NAME,
description="Monitoreo de ejecuciones de script_navegador: KPIs, tendencias, detalle de pasos y logs.",
)
dash_id = dashboard["id"]
print(f" Dashboard creado: id={dash_id}")
# Agregar cards al dashboard
dashcards = []
for idx, (card, card_def) in enumerate(created_cards):
dashcards.append({
"id": -(idx + 1),
"card_id": card["id"],
"size_x": card_def["size_x"],
"size_y": card_def["size_y"],
"col": card_def["col"],
"row": card_def["row"],
})
metabase_update_dashboard(client, dash_id, dashcards=dashcards)
print(f"\nDashboard listo: {METABASE_URL}/dashboard/{dash_id}")
client.close()
if __name__ == "__main__":
main()