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/.
This commit is contained in:
2026-03-29 00:14:07 +01:00
parent f570e783fe
commit 7495c3ea99
4 changed files with 263 additions and 1 deletions
@@ -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()