From 7495c3ea991574d1a576c00c648b878c51a9a9c5 Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Sun, 29 Mar 2026 00:14:07 +0100 Subject: [PATCH] feat: dashboard Metabase del registry + regla apps vs functions Script Python que crea un dashboard en Metabase con 15 cards: KPIs escalares, distribucion por lenguaje/dominio/kind/pureza, ranking de funciones mas usadas y complejas, cobertura de tests y tabla cruzada. Agrega regla apps_vs_functions que establece que codigo reutilizable va en functions/ y codigo especifico/hardcodeado va en apps/. --- .claude/CLAUDE.md | 2 +- .claude/rules/INDEX.md | 1 + .claude/rules/apps_vs_functions.md | 9 + .../create_registry_dashboard.py | 252 ++++++++++++++++++ 4 files changed, 263 insertions(+), 1 deletion(-) create mode 100644 .claude/rules/apps_vs_functions.md create mode 100644 apps/metabase_registry/create_registry_dashboard.py diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index ac9608ce..fb865fb2 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -66,7 +66,7 @@ fn-registry/ frontend/types/ # .ts + .md por tipo registry/ # Paquete Go: modelos, SQLite, parser, indexer, validacion, migraciones fn_operations/ # Paquete Go: operations database (libreria) - apps/ # Apps ejecutables (TUIs, CLIs) — modulos Go independientes, cada una con su operations.db + apps/ # Apps ejecutables (TUIs, CLIs, scripts) — codigo NO reutilizable, cada una con su operations.db cmd/fn/ # CLI principal docs/ # Specs de diseño docs/templates/ # Plantillas de frontmatter diff --git a/.claude/rules/INDEX.md b/.claude/rules/INDEX.md index 804af9e3..83dee87e 100644 --- a/.claude/rules/INDEX.md +++ b/.claude/rules/INDEX.md @@ -13,3 +13,4 @@ Reglas operativas del proyecto. Cada archivo es una regla independiente. | 07 | [proposals.md](proposals.md) | Quien crea proposals y cuando | | 08 | [tag_launcher.md](tag_launcher.md) | Tag launcher para Pipeline Launcher TUI | | 09 | [go_packages.md](go_packages.md) | Nombre de paquete Go = nombre del directorio | +| 10 | [apps_vs_functions.md](apps_vs_functions.md) | Codigo reutilizable en functions/, no reutilizable en apps/ | diff --git a/.claude/rules/apps_vs_functions.md b/.claude/rules/apps_vs_functions.md new file mode 100644 index 00000000..851b64e2 --- /dev/null +++ b/.claude/rules/apps_vs_functions.md @@ -0,0 +1,9 @@ +Solo codigo reutilizable y componible va en `functions/`, `python/functions/`, `bash/functions/`, `frontend/functions/`. + +Scripts especificos, dashboards hardcodeados, CLIs de un solo uso, y cualquier codigo que no sea una primitiva componible va en `apps/`. Cada app en `apps/` es independiente: puede importar funciones del registry pero nunca al reves. + +Criterios para decidir: +- **functions/**: firma generica, sin credenciales ni config hardcodeada, util en multiples contextos +- **apps/**: orquesta funciones del registry para un caso concreto, tiene config/credenciales, layout fijo + +Las apps Python importan funciones del registry con: `sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "python", "functions"))` y luego `from import ...` (sin prefijo `functions.`). diff --git a/apps/metabase_registry/create_registry_dashboard.py b/apps/metabase_registry/create_registry_dashboard.py new file mode 100644 index 00000000..5cf556d5 --- /dev/null +++ b/apps/metabase_registry/create_registry_dashboard.py @@ -0,0 +1,252 @@ +"""Crea un dashboard en Metabase con metricas del fn-registry.""" + +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, +) + +# --- Config --- +METABASE_URL = "http://localhost:3000" +EMAIL = "admin@fnregistry.local" +PASSWORD = "FnRegistry2024!" + +# --- SQL Queries --- +CARDS = [ + { + "name": "Total de Funciones", + "display": "scalar", + "sql": "SELECT COUNT(*) AS total FROM functions;", + "size_x": 4, "size_y": 3, "col": 0, "row": 0, + }, + { + "name": "Funciones con Tests", + "display": "scalar", + "sql": "SELECT COUNT(*) AS con_tests FROM functions WHERE tested = 1;", + "size_x": 4, "size_y": 3, "col": 4, "row": 0, + }, + { + "name": "Funciones sin Tests", + "display": "scalar", + "sql": "SELECT COUNT(*) AS sin_tests FROM functions WHERE tested = 0;", + "size_x": 4, "size_y": 3, "col": 8, "row": 0, + }, + { + "name": "Total de Tipos", + "display": "scalar", + "sql": "SELECT COUNT(*) AS total FROM types;", + "size_x": 3, "size_y": 3, "col": 12, "row": 0, + }, + { + "name": "Proposals Pendientes", + "display": "scalar", + "sql": "SELECT COUNT(*) AS pendientes FROM proposals WHERE status = 'pending';", + "size_x": 3, "size_y": 3, "col": 15, "row": 0, + }, + { + "name": "Funciones por Lenguaje", + "display": "bar", + "sql": "SELECT lang, COUNT(*) AS cantidad FROM functions GROUP BY lang ORDER BY cantidad DESC;", + "size_x": 6, "size_y": 5, "col": 0, "row": 3, + }, + { + "name": "Funciones por Dominio", + "display": "pie", + "sql": "SELECT domain, COUNT(*) AS cantidad FROM functions GROUP BY domain ORDER BY cantidad DESC;", + "size_x": 6, "size_y": 5, "col": 6, "row": 3, + }, + { + "name": "Funciones por Kind", + "display": "bar", + "sql": "SELECT kind, COUNT(*) AS cantidad FROM functions GROUP BY kind ORDER BY cantidad DESC;", + "size_x": 6, "size_y": 5, "col": 12, "row": 3, + }, + { + "name": "Puras vs Impuras", + "display": "pie", + "sql": "SELECT purity, COUNT(*) AS cantidad FROM functions GROUP BY purity ORDER BY cantidad DESC;", + "size_x": 6, "size_y": 5, "col": 0, "row": 8, + }, + { + "name": "Funciones Mas Usadas por Otras", + "display": "row", + "sql": """ + WITH RECURSIVE split_uses(fn_id, rest, val) AS ( + SELECT id, uses_functions || ',', NULL FROM functions WHERE uses_functions != '[]' AND uses_functions != '' + UNION ALL + SELECT fn_id, + SUBSTR(rest, INSTR(rest, ',') + 1), + TRIM(SUBSTR(rest, 1, INSTR(rest, ',') - 1), ' "[]') + FROM split_uses WHERE rest != '' + ) + SELECT val AS funcion_usada, COUNT(*) AS veces_usada + FROM split_uses + WHERE val IS NOT NULL AND val != '' AND val != ']' + GROUP BY val + ORDER BY veces_usada DESC + LIMIT 15; + """, + "size_x": 6, "size_y": 5, "col": 6, "row": 8, + }, + { + "name": "Funciones Mas Complejas (mas dependencias)", + "display": "row", + "sql": """ + SELECT + name || ' (' || lang || ')' AS funcion, + (LENGTH(uses_functions) - LENGTH(REPLACE(uses_functions, ',', '')) + + CASE WHEN uses_functions != '[]' AND uses_functions != '' THEN 1 ELSE 0 END) AS num_dependencias, + (LENGTH(uses_types) - LENGTH(REPLACE(uses_types, ',', '')) + + CASE WHEN uses_types != '[]' AND uses_types != '' THEN 1 ELSE 0 END) AS num_tipos + FROM functions + WHERE uses_functions != '[]' AND uses_functions != '' + ORDER BY num_dependencias DESC + LIMIT 15; + """, + "size_x": 6, "size_y": 5, "col": 12, "row": 8, + }, + { + "name": "Cobertura de Tests por Dominio", + "display": "bar", + "sql": """ + SELECT + domain, + SUM(CASE WHEN tested = 1 THEN 1 ELSE 0 END) AS con_tests, + SUM(CASE WHEN tested = 0 THEN 1 ELSE 0 END) AS sin_tests + FROM functions + GROUP BY domain + ORDER BY domain; + """, + "size_x": 9, "size_y": 5, "col": 0, "row": 13, + }, + { + "name": "Funciones por Lenguaje y Dominio", + "display": "table", + "sql": """ + SELECT + domain, + SUM(CASE WHEN lang = 'go' THEN 1 ELSE 0 END) AS go, + SUM(CASE WHEN lang = 'py' THEN 1 ELSE 0 END) AS python, + SUM(CASE WHEN lang = 'bash' THEN 1 ELSE 0 END) AS bash, + SUM(CASE WHEN lang = 'ts' THEN 1 ELSE 0 END) AS typescript, + COUNT(*) AS total + FROM functions + GROUP BY domain + ORDER BY total DESC; + """, + "size_x": 9, "size_y": 5, "col": 9, "row": 13, + }, + { + "name": "Tipos por Dominio y Algebraic", + "display": "table", + "sql": """ + SELECT + domain, + algebraic, + COUNT(*) AS cantidad + FROM types + GROUP BY domain, algebraic + ORDER BY domain, cantidad DESC; + """, + "size_x": 9, "size_y": 4, "col": 0, "row": 18, + }, + { + "name": "Funciones Recientes (ultimas 20 indexadas)", + "display": "table", + "sql": """ + SELECT name, lang, domain, kind, purity, tested + FROM functions + ORDER BY created_at DESC + LIMIT 20; + """, + "size_x": 9, "size_y": 4, "col": 9, "row": 18, + }, +] + + +def main(): + print("Autenticando en Metabase...") + client = metabase_auth(METABASE_URL, EMAIL, PASSWORD) + + # Encontrar la database registry.db + dbs = metabase_list_databases(client) + registry_db_id = None + for db in dbs: + if "registry" in db.get("name", "").lower() or ( + db.get("engine") == "sqlite" + and "registry" in db.get("details", {}).get("db", "") + ): + registry_db_id = db["id"] + print(f" Database encontrada: {db['name']} (id={db['id']})") + break + + if not registry_db_id: + print("ERROR: No se encontro registry.db en Metabase.") + print("Databases disponibles:") + for db in dbs: + print(f" - {db['id']}: {db['name']} ({db['engine']})") + sys.exit(1) + + # Verificar si ya existe un dashboard con este nombre + existing = metabase_list_dashboards(client) + for d in existing: + if d.get("name") == "fn-registry Overview": + 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": registry_db_id, + "type": "native", + "native": {"query": card_def["sql"]}, + }, + display=card_def["display"], + description=f"fn-registry: {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="fn-registry Overview", + description="Dashboard de metricas del registry: funciones, tipos, tests, dependencias y complejidad.", + ) + dash_id = dashboard["id"] + print(f" Dashboard creado: id={dash_id}") + + # Agregar cards al dashboard con posiciones + 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()