---
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://<subdomain>.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
