auto(0129): agents_dashboard — secret_store_cpp_infra + CMakeLists register #4
+1
-1
@@ -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
|
||||
|
||||
@@ -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/ |
|
||||
|
||||
@@ -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 <paquete> import ...` (sin prefijo `functions.`).
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user