--- name: render_glance_config kind: function lang: py domain: infra version: "1.1.0" purity: pure signature: "render_glance_config(services: list[dict], title: str = \"Procesos locales\", host_suffix: str = \"localhost\") -> str" description: "Transforma una lista de servicios normalizados en el YAML de configuración de Glance (dashboard self-hosted). Genera una página con un widget monitor por categoría que hace health-check de cada servicio y lo pinta verde/rojo. Función pura y determinista. Parte del sistema local_hub." tags: [local-hub, infra, glance, dashboard, yaml, config] uses_functions: [] uses_types: [] returns: [] returns_optional: false error_type: "" imports: [pyyaml] params: - name: services desc: "Lista de dicts de servicio normalizados. Cada uno requiere 'subdomain' (si falta, el servicio se ignora sin lanzar) y 'title'; opcional 'category' (default 'General'), 'icon' (se omite del site si está vacío o ausente) y 'health_path' (ruta de salud del servicio: si es distinta de '/', el site emite 'check-url' = url+health_path para que Glance haga el health-check ahí; si es '/' o falta, no se emite check-url)." - name: title desc: "Nombre de la página de Glance (campo 'name' de la página). Default 'Procesos locales'." - name: host_suffix desc: "Sufijo de host para las URLs de los sites. Default 'localhost' -> 'http://.localhost'." output: "String con el YAML completo de configuración de Glance (cabecera de comentario + pages/columns/widgets), terminado en '\\n'. Parseable con yaml.safe_load. Cada site lleva 'title' y 'url' (raíz del subdominio); además 'check-url' (url+health_path) cuando el servicio trae un health_path distinto de '/', e 'icon' cuando no está vacío." tested: true tests: - "test_golden_dos_categorias_dos_widgets" - "test_yaml_parseable_y_estructura" - "test_icon_omitido_cuando_vacio" - "test_host_suffix_custom" - "test_title_es_name_de_pagina" - "test_servicios_sin_subdomain_se_ignoran" - "test_determinismo" - "test_orden_sites_por_title" - "test_categoria_default_general" - "test_check_url_se_emite_cuando_health_path_no_es_raiz" test_file_path: "python/functions/infra/render_glance_config_test.py" file_path: "python/functions/infra/render_glance_config.py" --- ## Ejemplo ```python import sys, os sys.path.insert(0, os.path.join("python", "functions")) from infra.render_glance_config import render_glance_config services = [ {"subdomain": "metabase", "title": "Metabase", "icon": "si:metabase", "category": "Datos"}, {"subdomain": "jupyter", "title": "Jupyter Lab", "icon": "si:jupyter", "category": "Datos"}, {"subdomain": "portainer","title": "Portainer", "icon": "si:portainer", "category": "Infra"}, ] yaml_text = render_glance_config(services, title="Inicio") print(yaml_text) # pages: # - name: Inicio # columns: # - size: full # widgets: # - type: monitor # title: Datos # cache: 1m # sites: # - title: Jupyter Lab # ordenado por title dentro de la categoría # url: http://jupyter.localhost # icon: si:jupyter # - title: Metabase # url: http://metabase.localhost # icon: si:metabase # - type: monitor # title: Infra # cache: 1m # sites: # - title: Portainer # url: http://portainer.localhost # icon: si:portainer ``` ## Cuando usarla Cuando necesites regenerar el `glance.yml` del dashboard local a partir de `apps/local_hub/local_services.yaml`: tras añadir/quitar un servicio local, o en el pipeline `refresh_local_hub` que corre diario via dag_engine. La salida se escribe al archivo de config de Glance (el borde impuro: I/O lo hace el caller). ## Notas - **Decisión `title` -> `name`:** el parámetro `title` se usa como el campo `name` de la (única) página de Glance. No es un comentario de cabecera ni se ignora. Default `"Procesos locales"`. Así la firma queda útil sin añadir un parámetro extra para el nombre de página. - **Determinismo:** las categorías se ordenan alfabéticamente y los servicios de cada categoría por `title` (desempate por `subdomain`). Se serializa con `yaml.safe_dump(sort_keys=False)` sobre estructuras ya ordenadas, por lo que la misma entrada (en cualquier orden) produce siempre la misma salida byte a byte. - **Robustez:** los servicios sin `subdomain` se ignoran silenciosamente (no se lanza). El `icon` se omite del site cuando está vacío o ausente. Cada categoría produce un widget `type: monitor` con `cache: 1m`; todos los widgets van en una sola columna `size: full`. - **Función pura:** sin I/O, sin estado, determinista. El health-check real lo hace Glance en runtime (GET a `check-url` si existe, si no a `url`); esta función solo genera el texto. - **`check-url` vs `url`:** `url` es siempre la raíz del subdominio (lo que abre el usuario al clicar). `check-url` solo aparece cuando el servicio trae un `health_path` distinto de `/`, y vale `url + health_path`. Sirve para APIs que devuelven 404 en `/` pero 200 en su ruta de salud (ej. `/api/health`), de modo que Glance las pinta verde sin cambiar el enlace navegable. ## Capability growth log - v1.1.0 (2026-06-20) — añade check-url (health_path) por site para health-check preciso de APIs sin ruta raíz