"""Renderiza la configuración YAML de Glance a partir de servicios normalizados. Glance (https://github.com/glanceapp/glance) es un dashboard self-hosted. Este módulo transforma una lista de servicios en el YAML que Glance espera: una página con un widget `monitor` por categoría que hace health-check de cada servicio y lo pinta verde/rojo. Parte del sistema `local_hub`. """ import yaml _HEADER = ( "# Generado por render_glance_config_py_infra — NO editar a mano. " "Fuente: apps/local_hub/local_services.yaml\n" ) def render_glance_config( services: list[dict], title: str = "Procesos locales", host_suffix: str = "localhost", ) -> str: """Construye el YAML de configuración de Glance para una lista de servicios. Función pura y determinista: agrupa los servicios por su clave ``category``, crea un widget ``type: monitor`` por categoría (ordenadas alfabéticamente) y dentro de cada uno un site por servicio (ordenados por ``title``). Cada site apunta a ``http://.`` (campo ``url``, lo que abre el usuario al clicar). Si el servicio trae un ``health_path`` distinto de ``"/"``, el site añade además ``check-url`` = ``url + health_path``: es la ruta que Glance usa para el health-check (muchas APIs dan 404 en ``/`` pero 200 en ``/api/health``), sin cambiar el enlace que ve el usuario. Args: services: lista de dicts de servicio normalizados. Cada uno debe traer al menos ``subdomain`` (si falta, el servicio se ignora sin lanzar) y ``title``; opcionalmente ``category`` (default ``"General"``), ``icon`` (se omite del site si está vacío o ausente) y ``health_path`` (si es distinto de ``"/"``, el site emite ``check-url`` = url + health_path; si es ``"/"`` o falta, no se emite ``check-url``). title: nombre de la página de Glance (campo ``name`` de la página). Default ``"Procesos locales"``. host_suffix: sufijo de host para las URLs de los sites. Default ``"localhost"`` -> ``http://.localhost``. Returns: String con el YAML completo de Glance, terminado en ``\\n``. """ # Agrupa por categoría, ignorando servicios sin subdomain. by_category: dict[str, list[dict]] = {} for svc in services: subdomain = svc.get("subdomain") if not subdomain: continue category = svc.get("category") or "General" by_category.setdefault(category, []).append(svc) widgets: list[dict] = [] for category in sorted(by_category.keys()): svcs = sorted( by_category[category], key=lambda s: (s.get("title") or "", s.get("subdomain") or ""), ) sites: list[dict] = [] for svc in svcs: url = f"http://{svc['subdomain']}.{host_suffix}" site: dict = { "title": svc.get("title") or svc["subdomain"], "url": url, } # El health-check apunta al health_path del servicio (no a "/"). # Muchas APIs devuelven 404 en la raiz pero 200 en su ruta de salud # (ej. /api/health), asi Glance las pinta verde correctamente. El # campo `url` (lo que abre el usuario al clicar) sigue siendo la raiz. health = svc.get("health_path") or "/" if health and health != "/": site["check-url"] = url + health icon = svc.get("icon") if icon: site["icon"] = icon sites.append(site) widgets.append( { "type": "monitor", "title": category, "cache": "1m", "sites": sites, } ) config = { "pages": [ { "name": title, "columns": [ { "size": "full", "widgets": widgets, } ], } ] } body = yaml.safe_dump( config, sort_keys=False, default_flow_style=False, allow_unicode=True, ) return _HEADER + body